diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts index 1dacf1bd2..a151d5837 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts @@ -1,6 +1,7 @@ import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router"; +import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router"; @@ -12,5 +13,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record< > = { [SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter, [SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter, - [SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter + [SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter, + [SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter }; diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/ldap-password-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/ldap-password-rotation-router.ts new file mode 100644 index 000000000..04d2b50ac --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/ldap-password-rotation-router.ts @@ -0,0 +1,19 @@ +import { + CreateLdapPasswordRotationSchema, + LdapPasswordRotationGeneratedCredentialsSchema, + LdapPasswordRotationSchema, + UpdateLdapPasswordRotationSchema +} from "@app/ee/services/secret-rotation-v2/ldap-password"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerLdapPasswordRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.LdapPassword, + server, + responseSchema: LdapPasswordRotationSchema, + createSchema: CreateLdapPasswordRotationSchema, + updateSchema: UpdateLdapPasswordRotationSchema, + generatedCredentialsSchema: LdapPasswordRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts index bfb8b38c0..55183e25f 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; +import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; @@ -13,7 +14,8 @@ import { AuthMode } from "@app/services/auth/auth-type"; const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [ PostgresCredentialsRotationListItemSchema, MsSqlCredentialsRotationListItemSchema, - Auth0ClientSecretRotationListItemSchema + Auth0ClientSecretRotationListItemSchema, + LdapPasswordRotationListItemSchema ]); export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/index.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/index.ts new file mode 100644 index 000000000..55929169a --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/index.ts @@ -0,0 +1,3 @@ +export * from "./ldap-password-rotation-constants"; +export * from "./ldap-password-rotation-schemas"; +export * from "./ldap-password-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-constants.ts new file mode 100644 index 000000000..b46165c71 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-constants.ts @@ -0,0 +1,15 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const LDAP_PASSWORD_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "LDAP Password", + type: SecretRotation.LdapPassword, + connection: AppConnection.Ldap, + template: { + secretsMapping: { + dn: "LDAP_DN", + password: "LDAP_PASSWORD" + } + } +}; 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 new file mode 100644 index 000000000..0fd01b753 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts @@ -0,0 +1,181 @@ +import ldap from "ldapjs"; + +import { + TRotationFactory, + TRotationFactoryGetSecretsPayload, + TRotationFactoryIssueCredentials, + TRotationFactoryRevokeCredentials, + TRotationFactoryRotateCredentials +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { logger } from "@app/lib/logger"; +import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; +import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap"; + +import { generatePassword } from "../shared/utils"; +import { + TLdapPasswordRotationGeneratedCredentials, + TLdapPasswordRotationWithConnection +} from "./ldap-password-rotation-types"; + +const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le"); + +export const ldapPasswordRotationFactory: TRotationFactory< + TLdapPasswordRotationWithConnection, + TLdapPasswordRotationGeneratedCredentials +> = (secretRotation, appConnectionDAL, kmsService) => { + const { + connection, + parameters: { dn, passwordRequirements }, + secretsMapping + } = secretRotation; + + const $verifyCredentials = async (credentials: Pick) => { + try { + const client = await getLdapConnectionClient({ ...connection.credentials, ...credentials }); + + client.unbind(); + client.destroy(); + } catch (error) { + throw new Error(`Failed to verify credentials - ${(error as Error).message}`); + } + }; + + const $rotatePassword = async () => { + const { credentials, orgId } = connection; + + if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection"); + + const client = await getLdapConnectionClient(credentials); + const isPersonalRotation = credentials.dn === dn; + + const password = generatePassword(passwordRequirements); + + let changes: ldap.Change[] | ldap.Change; + + switch (credentials.provider) { + case LdapProvider.ActiveDirectory: + { + const encodedPassword = getEncodedPassword(password); + + // service account vs personal password rotation require different changes + if (isPersonalRotation) { + const currentEncodedPassword = getEncodedPassword(credentials.password); + + changes = [ + new ldap.Change({ + operation: "delete", + modification: { + type: "unicodePwd", + values: [currentEncodedPassword] + } + }), + new ldap.Change({ + operation: "add", + modification: { + type: "unicodePwd", + values: [encodedPassword] + } + }) + ]; + } else { + changes = new ldap.Change({ + operation: "replace", + modification: { + type: "unicodePwd", + values: [encodedPassword] + } + }); + } + } + break; + default: + throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`); + } + + try { + await new Promise((resolve, reject) => { + client.modify(dn, changes, (err) => { + if (err) { + logger.error(err, "LDAP Password Rotation Failed"); + reject(new Error(`Provider Modify Error: ${err.message}`)); + } else { + resolve(true); + } + }); + }); + } finally { + client.unbind(); + client.destroy(); + } + + await $verifyCredentials({ dn, password }); + + if (isPersonalRotation) { + const updatedCredentials: TLdapConnection["credentials"] = { + ...credentials, + password + }; + + const encryptedCredentials = await encryptAppConnectionCredentials({ + credentials: updatedCredentials, + orgId, + kmsService + }); + + await appConnectionDAL.updateById(connection.id, { encryptedCredentials }); + } + + return { dn, password }; + }; + + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { + const credentials = await $rotatePassword(); + + return callback(credentials); + }; + + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + _, + callback + ) => { + // we just rotate to a new password, essentially revoking old credentials + await $rotatePassword(); + + return callback(); + }; + + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + _, + callback + ) => { + const credentials = await $rotatePassword(); + + return callback(credentials); + }; + + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ( + generatedCredentials + ) => { + const secrets = [ + { + key: secretsMapping.dn, + value: generatedCredentials.dn + }, + { + key: secretsMapping.password, + value: generatedCredentials.password + } + ]; + + return secrets; + }; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-schemas.ts new file mode 100644 index 000000000..2fc832c14 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-schemas.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general"; +import { SecretRotations } from "@app/lib/api-docs"; +import { SecretNameSchema } from "@app/server/lib/schemas"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const LdapPasswordRotationGeneratedCredentialsSchema = z + .object({ + dn: z.string(), + password: z.string() + }) + .array() + .min(1) + .max(2); + +const LdapPasswordRotationParametersSchema = z.object({ + dn: z + .string() + .trim() + .min(1, "Distinguished Name (DN) Required") + .describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn), + passwordRequirements: PasswordRequirementsSchema.optional() +}); + +const LdapPasswordRotationSecretsMappingSchema = z.object({ + dn: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.dn), + password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.password) +}); + +export const LdapPasswordRotationTemplateSchema = z.object({ + secretsMapping: z.object({ + dn: z.string(), + password: z.string() + }) +}); + +export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotation.LdapPassword).extend({ + type: z.literal(SecretRotation.LdapPassword), + parameters: LdapPasswordRotationParametersSchema, + secretsMapping: LdapPasswordRotationSecretsMappingSchema +}); + +export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({ + parameters: LdapPasswordRotationParametersSchema, + secretsMapping: LdapPasswordRotationSecretsMappingSchema +}); + +export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({ + parameters: LdapPasswordRotationParametersSchema.optional(), + secretsMapping: LdapPasswordRotationSecretsMappingSchema.optional() +}); + +export const LdapPasswordRotationListItemSchema = z.object({ + name: z.literal("LDAP Password"), + connection: z.literal(AppConnection.Ldap), + type: z.literal(SecretRotation.LdapPassword), + template: LdapPasswordRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types.ts new file mode 100644 index 000000000..cb15b0734 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +import { TLdapConnection } from "@app/services/app-connection/ldap"; + +import { + CreateLdapPasswordRotationSchema, + LdapPasswordRotationGeneratedCredentialsSchema, + LdapPasswordRotationListItemSchema, + LdapPasswordRotationSchema +} from "./ldap-password-rotation-schemas"; + +export type TLdapPasswordRotation = z.infer; + +export type TLdapPasswordRotationInput = z.infer; + +export type TLdapPasswordRotationListItem = z.infer; + +export type TLdapPasswordRotationWithConnection = TLdapPasswordRotation & { + connection: TLdapConnection; +}; + +export type TLdapPasswordRotationGeneratedCredentials = z.infer; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts index d43cacb3a..a094af635 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts @@ -1,7 +1,8 @@ export enum SecretRotation { PostgresCredentials = "postgres-credentials", MsSqlCredentials = "mssql-credentials", - Auth0ClientSecret = "auth0-client-secret" + Auth0ClientSecret = "auth0-client-secret", + LdapPassword = "ldap-password" } export enum SecretRotationStatus { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index 603b77cc1..b1b0e4231 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -4,6 +4,7 @@ import { getConfig } from "@app/lib/config/env"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret"; +import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; @@ -18,7 +19,8 @@ import { const SECRET_ROTATION_LIST_OPTIONS: Record = { [SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION, [SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION, - [SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION + [SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION, + [SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION }; export const listSecretRotationOptions = () => { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts index 1050c3419..c1bdcce99 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts @@ -4,11 +4,13 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums export const SECRET_ROTATION_NAME_MAP: Record = { [SecretRotation.PostgresCredentials]: "PostgreSQL Credentials", [SecretRotation.MsSqlCredentials]: "Microsoft SQL Sever Credentials", - [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret" + [SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret", + [SecretRotation.LdapPassword]: "LDAP Password" }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { [SecretRotation.PostgresCredentials]: AppConnection.Postgres, [SecretRotation.MsSqlCredentials]: AppConnection.MsSql, - [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0 + [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0, + [SecretRotation.LdapPassword]: AppConnection.Ldap }; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts index a828acb32..94d23e385 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts @@ -14,6 +14,7 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns"; +import { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns"; import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { calculateNextRotationAt, @@ -114,7 +115,8 @@ type TRotationFactoryImplementation = TRotationFactory< const SECRET_ROTATION_FACTORY_MAP: Record = { [SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, [SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, - [SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation + [SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation, + [SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation }; export const secretRotationV2ServiceFactory = ({ @@ -449,6 +451,18 @@ export const secretRotationV2ServiceFactory = ({ kmsService ); + // even though we have a db constraint we want to check before any rotation of credentials is attempted + // to prevent creation failure after external credentials have been modified + const conflictingRotation = await secretRotationV2DAL.findOne({ + name: payload.name, + folderId: folder.id + }); + + if (conflictingRotation) + throw new BadRequestError({ + message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${secretPath}"` + }); + try { const currentTime = new Date(); diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts index c52fa5465..340c82886 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts @@ -12,6 +12,13 @@ import { TAuth0ClientSecretRotationListItem, TAuth0ClientSecretRotationWithConnection } from "./auth0-client-secret"; +import { + TLdapPasswordRotation, + TLdapPasswordRotationGeneratedCredentials, + TLdapPasswordRotationInput, + TLdapPasswordRotationListItem, + TLdapPasswordRotationWithConnection +} from "./ldap-password"; import { TMsSqlCredentialsRotation, TMsSqlCredentialsRotationInput, @@ -27,26 +34,34 @@ import { import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; import { SecretRotation } from "./secret-rotation-v2-enums"; -export type TSecretRotationV2 = TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation; +export type TSecretRotationV2 = + | TPostgresCredentialsRotation + | TMsSqlCredentialsRotation + | TAuth0ClientSecretRotation + | TLdapPasswordRotation; export type TSecretRotationV2WithConnection = | TPostgresCredentialsRotationWithConnection | TMsSqlCredentialsRotationWithConnection - | TAuth0ClientSecretRotationWithConnection; + | TAuth0ClientSecretRotationWithConnection + | TLdapPasswordRotationWithConnection; export type TSecretRotationV2GeneratedCredentials = | TSqlCredentialsRotationGeneratedCredentials - | TAuth0ClientSecretRotationGeneratedCredentials; + | TAuth0ClientSecretRotationGeneratedCredentials + | TLdapPasswordRotationGeneratedCredentials; export type TSecretRotationV2Input = | TPostgresCredentialsRotationInput | TMsSqlCredentialsRotationInput - | TAuth0ClientSecretRotationInput; + | TAuth0ClientSecretRotationInput + | TLdapPasswordRotationInput; export type TSecretRotationV2ListItem = | TPostgresCredentialsRotationListItem | TMsSqlCredentialsRotationListItem - | TAuth0ClientSecretRotationListItem; + | TAuth0ClientSecretRotationListItem + | TLdapPasswordRotationListItem; export type TSecretRotationV2Raw = NonNullable>>; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts index 2db9c0251..2aff4347c 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts @@ -1,11 +1,13 @@ import { z } from "zod"; import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret"; +import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials"; export const SecretRotationV2Schema = z.discriminatedUnion("type", [ PostgresCredentialsRotationSchema, MsSqlCredentialsRotationSchema, - Auth0ClientSecretRotationSchema + Auth0ClientSecretRotationSchema, + LdapPasswordRotationSchema ]); diff --git a/backend/src/ee/services/secret-rotation-v2/shared/general/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/general/index.ts new file mode 100644 index 000000000..9b2148414 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/general/index.ts @@ -0,0 +1 @@ +export * from "./password-requirements-schema"; diff --git a/backend/src/ee/services/secret-rotation-v2/shared/general/password-requirements-schema.ts b/backend/src/ee/services/secret-rotation-v2/shared/general/password-requirements-schema.ts new file mode 100644 index 000000000..086e718f5 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/general/password-requirements-schema.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; + +import { SecretRotations } from "@app/lib/api-docs"; + +export const PasswordRequirementsSchema = z + .object({ + length: z + .number() + .min(1, "Password length must be a positive number") + .max(250, "Password length must be less than 250") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.length), + required: z.object({ + digits: z + .number() + .min(0, "Digit count must be non-negative") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.digits), + lowercase: z + .number() + .min(0, "Lowercase count must be non-negative") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.lowercase), + uppercase: z + .number() + .min(0, "Uppercase count must be non-negative") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.uppercase), + symbols: z + .number() + .min(0, "Symbol count must be non-negative") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.symbols) + }), + allowedSymbols: z + .string() + .optional() + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.allowedSymbols) + }) + .refine((data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; + }, "Sum of required characters cannot exceed the total length") + .describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.base); diff --git a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts index dfe4c22ed..9b2eb7839 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts @@ -1,6 +1,17 @@ import { randomInt } from "crypto"; -const DEFAULT_PASSWORD_REQUIREMENTS = { +type TPasswordRequirements = { + length: number; + required: { + lowercase: number; + uppercase: number; + digits: number; + symbols: number; + }; + allowedSymbols?: string; +}; + +const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = { length: 48, required: { lowercase: 1, @@ -11,9 +22,9 @@ const DEFAULT_PASSWORD_REQUIREMENTS = { allowedSymbols: "-_.~!*" }; -export const generatePassword = () => { +export const generatePassword = (passwordRequirements?: TPasswordRequirements) => { try { - const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS; + const { length, required, allowedSymbols } = passwordRequirements ?? DEFAULT_PASSWORD_REQUIREMENTS; const chars = { lowercase: "abcdefghijklmnopqrstuvwxyz", diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index ff07940bc..fd72268a8 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1812,6 +1812,16 @@ export const AppConnections = { WINDMILL: { instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).", accessToken: "The access token to use to connect with Windmill." + }, + LDAP: { + provider: "The type of LDAP provider. Determines provider-specific behaviors.", + url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').", + dn: "The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').", + password: "The password to bind with for authentication.", + sslRejectUnauthorized: + "Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.", + sslCertificate: + "The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate." } } }; @@ -2015,6 +2025,22 @@ export const SecretRotations = { }, AUTH0_CLIENT_SECRET: { clientId: "The client ID of the Auth0 Application to rotate the client secret for." + }, + LDAP_PASSWORD: { + dn: "The Distinguished Name (DN) of the principal to rotate the password for." + }, + GENERAL: { + PASSWORD_REQUIREMENTS: { + base: "The password requirements to use when generating the new password.", + length: "The length of the password to generate.", + required: { + digits: "The amount of digits to require in the generated password.", + lowercase: "The amount of lowercase characters to require in the generated password.", + uppercase: "The amount of uppercase characters to require in the generated password.", + symbols: "The amount of symbols to require in the generated password." + }, + allowedSymbols: 'The allowed symbols to use in the generated password (defaults to "-_.~!*").' + } } }, SECRETS_MAPPING: { @@ -2025,6 +2051,10 @@ export const SecretRotations = { AUTH0_CLIENT_SECRET: { clientId: "The name of the secret that the client ID will be mapped to.", clientSecret: "The name of the secret that the rotated client secret will be mapped to." + }, + LDAP_PASSWORD: { + dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.", + password: "The name of the secret that the rotated password will be mapped to." } } }; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index c8d2a976d..50cb011b5 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -27,6 +27,7 @@ import { HumanitecConnectionListItemSchema, SanitizedHumanitecConnectionSchema } from "@app/services/app-connection/humanitec"; +import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; import { PostgresConnectionListItemSchema, @@ -58,7 +59,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedMsSqlConnectionSchema.options, ...SanitizedCamundaConnectionSchema.options, ...SanitizedWindmillConnectionSchema.options, - ...SanitizedAuth0ConnectionSchema.options + ...SanitizedAuth0ConnectionSchema.options, + ...SanitizedLdapConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -75,7 +77,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ MsSqlConnectionListItemSchema, CamundaConnectionListItemSchema, WindmillConnectionListItemSchema, - Auth0ConnectionListItemSchema + Auth0ConnectionListItemSchema, + LdapConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index a833b6882..9c6b0065e 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -1,6 +1,6 @@ -import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { registerAuth0ConnectionRouter } from "./auth0-connection-router"; import { registerAwsConnectionRouter } from "./aws-connection-router"; import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router"; import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; @@ -9,6 +9,7 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; +import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; @@ -32,5 +33,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.Ldap, + server, + sanitizedResponseSchema: SanitizedLdapConnectionSchema, + createSchema: CreateLdapConnectionSchema, + updateSchema: UpdateLdapConnectionSchema + }); +}; diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 6b6048f2a..b4404c394 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -12,7 +12,8 @@ export enum AppConnection { MsSql = "mssql", Camunda = "camunda", Windmill = "windmill", - Auth0 = "auth0" + Auth0 = "auth0", + Ldap = "ldap" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 7e08a92b4..e444ea21a 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -41,6 +41,7 @@ import { HumanitecConnectionMethod, validateHumanitecConnectionCredentials } from "./humanitec"; +import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { @@ -71,7 +72,8 @@ export const listAppConnectionOptions = () => { getMsSqlConnectionListItem(), getCamundaConnectionListItem(), getWindmillConnectionListItem(), - getAuth0ConnectionListItem() + getAuth0ConnectionListItem(), + getLdapConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -135,7 +137,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Ldap]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -170,6 +173,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => return "Access Token"; case Auth0ConnectionMethod.ClientCredentials: return "Client Credentials"; + case LdapConnectionMethod.SimpleBind: + return "Simple Bind"; default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Unhandled App Connection Method: ${method}`); @@ -214,5 +219,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Camunda]: platformManagedCredentialsNotSupported, [AppConnection.Vercel]: platformManagedCredentialsNotSupported, [AppConnection.Windmill]: platformManagedCredentialsNotSupported, - [AppConnection.Auth0]: platformManagedCredentialsNotSupported + [AppConnection.Auth0]: platformManagedCredentialsNotSupported, + [AppConnection.Ldap]: platformManagedCredentialsNotSupported // we could support this in the future }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 762a9bcf2..886aa4955 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -14,5 +14,6 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.MsSql]: "Microsoft SQL Server", [AppConnection.Camunda]: "Camunda", [AppConnection.Windmill]: "Windmill", - [AppConnection.Auth0]: "Auth0" + [AppConnection.Auth0]: "Auth0", + [AppConnection.Ldap]: "LDAP" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 5293761a8..27152da52 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -43,6 +43,7 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github"; import { githubConnectionService } from "./github/github-connection-service"; import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; +import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud"; @@ -74,7 +75,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record>>; @@ -118,6 +125,7 @@ export type TAppConnectionInput = { id: string } & ( | TCamundaConnectionInput | TWindmillConnectionInput | TAuth0ConnectionInput + | TLdapConnectionInput ); export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput; @@ -144,7 +152,8 @@ export type TAppConnectionConfig = | TSqlConnectionConfig | TCamundaConnectionConfig | TWindmillConnectionConfig - | TAuth0ConnectionConfig; + | TAuth0ConnectionConfig + | TLdapConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -160,7 +169,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateTerraformCloudConnectionCredentialsSchema | TValidateVercelConnectionCredentialsSchema | TValidateWindmillConnectionCredentialsSchema - | TValidateAuth0ConnectionCredentialsSchema; + | TValidateAuth0ConnectionCredentialsSchema + | TValidateLdapConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/ldap/index.ts b/backend/src/services/app-connection/ldap/index.ts new file mode 100644 index 000000000..639879a08 --- /dev/null +++ b/backend/src/services/app-connection/ldap/index.ts @@ -0,0 +1,4 @@ +export * from "./ldap-connection-enums"; +export * from "./ldap-connection-fns"; +export * from "./ldap-connection-schemas"; +export * from "./ldap-connection-types"; diff --git a/backend/src/services/app-connection/ldap/ldap-connection-enums.ts b/backend/src/services/app-connection/ldap/ldap-connection-enums.ts new file mode 100644 index 000000000..9d6a3d5cc --- /dev/null +++ b/backend/src/services/app-connection/ldap/ldap-connection-enums.ts @@ -0,0 +1,7 @@ +export enum LdapConnectionMethod { + SimpleBind = "simple-bind" +} + +export enum LdapProvider { + ActiveDirectory = "active-directory" +} diff --git a/backend/src/services/app-connection/ldap/ldap-connection-fns.ts b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts new file mode 100644 index 000000000..10da004eb --- /dev/null +++ b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts @@ -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((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(); + } +}; diff --git a/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts new file mode 100644 index 000000000..6cd957c15 --- /dev/null +++ b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts @@ -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() +}); diff --git a/backend/src/services/app-connection/ldap/ldap-connection-types.ts b/backend/src/services/app-connection/ldap/ldap-connection-types.ts new file mode 100644 index 000000000..4d324a932 --- /dev/null +++ b/backend/src/services/app-connection/ldap/ldap-connection-types.ts @@ -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; + +export type TLdapConnectionInput = z.infer & { + app: AppConnection.Ldap; +}; + +export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema; + +export type TLdapConnectionConfig = DiscriminativePick & { + orgId: string; +}; diff --git a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts index 38ef0eef6..994f9a40d 100644 --- a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts +++ b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts @@ -31,7 +31,8 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [ port: true, username: true, sslEnabled: true, - sslRejectUnauthorized: true + sslRejectUnauthorized: true, + sslCertificate: true }) }) ]); diff --git a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts index 510f7b7d0..1ddf1e2da 100644 --- a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts +++ b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts @@ -29,7 +29,8 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method", port: true, username: true, sslEnabled: true, - sslRejectUnauthorized: true + sslRejectUnauthorized: true, + sslCertificate: true }) }) ]); diff --git a/docs/api-reference/endpoints/app-connections/ldap/available.mdx b/docs/api-reference/endpoints/app-connections/ldap/available.mdx new file mode 100644 index 000000000..b42f2bc3d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/ldap/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/create.mdx b/docs/api-reference/endpoints/app-connections/ldap/create.mdx new file mode 100644 index 000000000..181a76902 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/ldap" +--- + + + Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/ldap/delete.mdx b/docs/api-reference/endpoints/app-connections/ldap/delete.mdx new file mode 100644 index 000000000..4888fd04d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/ldap/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/ldap/get-by-id.mdx new file mode 100644 index 000000000..7c4524ed8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/ldap/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/ldap/get-by-name.mdx new file mode 100644 index 000000000..dc7516bba --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/ldap/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/list.mdx b/docs/api-reference/endpoints/app-connections/ldap/list.mdx new file mode 100644 index 000000000..e909c9266 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/ldap" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/update.mdx b/docs/api-reference/endpoints/app-connections/ldap/update.mdx new file mode 100644 index 000000000..06c7f7f77 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/ldap/{connectionId}" +--- + + + Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/create.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/create.mdx new file mode 100644 index 000000000..682b531ad --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v2/secret-rotations/ldap-password" +--- + + + Check out the configuration docs for [LDAP Password Rotations](/documentation/platform/secret-rotation/ldap-password) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/delete.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/delete.mdx new file mode 100644 index 000000000..d4cee951f --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/secret-rotations/ldap-password/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-id.mdx new file mode 100644 index 000000000..f422d036d --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v2/secret-rotations/ldap-password/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-name.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-name.mdx new file mode 100644 index 000000000..68de6a722 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v2/secret-rotations/ldap-password/rotation-name/{rotationName}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id.mdx new file mode 100644 index 000000000..6aed49218 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Credentials by ID" +openapi: "GET /api/v2/secret-rotations/ldap-password/{rotationId}/generated-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/list.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/list.mdx new file mode 100644 index 000000000..bf2bb5562 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-rotations/ldap-password" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets.mdx new file mode 100644 index 000000000..8ad2ae52b --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Rotate Secrets" +openapi: "POST /api/v2/secret-rotations/ldap-password/{rotationId}/rotate-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/update.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/update.mdx new file mode 100644 index 000000000..b59ea5250 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/secret-rotations/ldap-password/{rotationId}" +--- + + + Check out the configuration docs for [LDAP Rotations](/documentation/platform/secret-rotation/ldap-password) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx b/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx index 3845a3879..0fd43f2c4 100644 --- a/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx +++ b/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx @@ -1,5 +1,5 @@ --- -title: "Auth0 Client Secret" +title: "Auth0 Client Secret Rotation" description: "Learn how to automatically rotate Auth0 Client Secrets." --- diff --git a/docs/documentation/platform/secret-rotation/ldap-password.mdx b/docs/documentation/platform/secret-rotation/ldap-password.mdx new file mode 100644 index 000000000..53802b8a9 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/ldap-password.mdx @@ -0,0 +1,173 @@ +--- +title: "LDAP Password Rotation" +description: "Learn how to automatically rotate LDAP passwords." +--- + + + Due to how LDAP passwords are rotated, retired credentials will not be able to + authenticate with the LDAP provider during their [inactive period](./overview#how-rotation-works). + + This is a limitation of the LDAP provider and cannot be + rectified by Infisical. + + +## Prerequisites + +- Create an [LDAP Connection](/integrations/app-connections/ldap) with the **Secret Rotation** requirements + +## Create an LDAP Password Rotation in Infisical + + + + 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown. + ![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png) + + 2. Select the **LDAP Password** option. + ![Select LDAP Password](/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png) + + 3. Select the **LDAP Connection** to use and configure the rotation behavior. Then click **Next**. + ![Rotation Configuration](/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png) + + - **LDAP Connection** - the connection that will perform the rotation of the configured DN's password. + + LDAP Password Rotations require an LDAP Connection that uses ldaps:// protocol. + + - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation. + - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed. + - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation. + + Due to LDAP Password Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option. + + + + 4. Specify the Distinguished Name (DN) of the principal whose password you want to rotate and configure the password requirements. Then click **Next**. + ![Rotation Parameters](/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png) + + 5. Specify the secret names that the client credentials should be mapped to. Then click **Next**. + ![Rotation Secrets Mapping](/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png) + + - **DN** - the name of the secret that the principal's Distinguished Name (DN) will be mapped to. + - **Password** - the name of the secret that the rotated password will be mapped to. + + 6. Give your rotation a name and description (optional). Then click **Next**. + ![Rotation Details](/images/secret-rotations-v2/ldap-password/ldap-password-details.png) + + - **Name** - the name of the secret rotation configuration. Must be slug-friendly. + - **Description** (optional) - a description of this rotation configuration. + + 7. Review your configuration, then click **Create Secret Rotation**. + ![Rotation Review](/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png) + + 8. Your **LDAP Password** credentials are now available for use via the mapped secrets. + ![Rotation Created](/images/secret-rotations-v2/ldap-password/ldap-password-created.png) + + + To create an LDAP Password Rotation, make an API request to the [Create LDAP + Password Rotation](/api-reference/endpoints/secret-rotations/ldap-password/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-rotations/ldap-password \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-ldap-rotation", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "my ldap password rotation", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isAutoRotationEnabled": false, + "rotationInterval": 30, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "parameters": { + "dn": "CN=John,CN=Users,DC=example,DC=com", + "passwordRequirements": { + "length": 48, + "required": { + "digits": 2, + "lowercase": 2, + "uppercase": 2, + "symbols": 2 + }, + "allowedSymbols": "-_.~!*" + } + }, + "secretsMapping": { + "dn": "LDAP_DN", + "password": "LDAP_PASSWORD" + } + }' + ``` + + + Due to LDAP Password Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option. + + + ### Sample response + + ```bash Response + { + "secretRotation": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-auth0-rotation", + "description": "my client secret rotation", + "secretsMapping": { + "dn": "LDAP_DN", + "password": "LDAP_PASSWORD" + }, + "isAutoRotationEnabled": false, + "activeIndex": 0, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "rotationInterval": 30, + "rotationStatus": "success", + "lastRotationAttemptedAt": "2023-11-07T05:31:56Z", + "lastRotatedAt": "2023-11-07T05:31:56Z", + "lastRotationJobId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "nextRotationAt": "2023-11-07T05:31:56Z", + "connection": { + "app": "auth0", + "name": "my-auth0-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "lastRotationMessage": null, + "type": "ldap-password", + "parameters": { + "dn": "CN=John,CN=Users,DC=example,DC=com", + "passwordRequirements": { + "length": 48, + "required": { + "digits": 2, + "lowercase": 2, + "uppercase": 2, + "symbols": 2 + }, + "allowedSymbols": "-_.~!*" + } + } + } + } + ``` + + diff --git a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx index 8789e4152..c20622f26 100644 --- a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx @@ -1,5 +1,5 @@ --- -title: "Microsoft SQL Server Credentials" +title: "Microsoft SQL Server Credentials Rotation" description: "Learn how to automatically rotate Microsoft SQL Server credentials." --- diff --git a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx index e0606e6ab..55175d967 100644 --- a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx @@ -1,5 +1,5 @@ --- -title: "PostgreSQL Credentials" +title: "PostgreSQL Credentials Rotation" description: "Learn how to automatically rotate PostgreSQL credentials." --- diff --git a/docs/images/app-connections/ldap/create-simple-bind-method.png b/docs/images/app-connections/ldap/create-simple-bind-method.png new file mode 100644 index 000000000..e7fff6789 Binary files /dev/null and b/docs/images/app-connections/ldap/create-simple-bind-method.png differ diff --git a/docs/images/app-connections/ldap/select-ldap-connection.png b/docs/images/app-connections/ldap/select-ldap-connection.png new file mode 100644 index 000000000..48465df67 Binary files /dev/null and b/docs/images/app-connections/ldap/select-ldap-connection.png differ diff --git a/docs/images/app-connections/ldap/simple-bind-connection.png b/docs/images/app-connections/ldap/simple-bind-connection.png new file mode 100644 index 000000000..8013a4687 Binary files /dev/null and b/docs/images/app-connections/ldap/simple-bind-connection.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png new file mode 100644 index 000000000..91a2f2fb3 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png new file mode 100644 index 000000000..1725c4355 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-created.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-created.png new file mode 100644 index 000000000..4172ec7f7 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-created.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-details.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-details.png new file mode 100644 index 000000000..ed41c13ad Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-details.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png new file mode 100644 index 000000000..dfe723b06 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png new file mode 100644 index 000000000..997073bc5 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png b/docs/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png new file mode 100644 index 000000000..4dd500fe1 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png differ diff --git a/docs/integrations/app-connections/ldap.mdx b/docs/integrations/app-connections/ldap.mdx new file mode 100644 index 000000000..de96173cc --- /dev/null +++ b/docs/integrations/app-connections/ldap.mdx @@ -0,0 +1,96 @@ +--- +title: "LDAP Connection" +description: "Learn how to configure an LDAP Connection for Infisical." +--- + +Infisical supports the use of [Simple Binding](https://ldap.com/the-ldap-bind-operation) to connect with your LDAP provider. + +## Prerequisites + +You will need the following information to establish an LDAP connection: + +- **LDAP URL** - The LDAP/LDAPS URL to connect to (e.g., ldap://domain-or-ip:389 or ldaps://domain-or-ip:636) +- **Binding DN** - The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com') +- **Binding Password** - The password to bind with for authentication +- **CA Certificate** - The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate + +Depending on how you intend to use your LDAP connection, there may be additional requirements: + + + + + For Password Rotation, the following requirements must additionally be met: + - You must use an LDAPS connection + - The binding user must either have: + - Permission to change other users passwords if rotating directory users' passwords + - Permission to update their own password if rotating their personal password + + + + + +## Setup LDAP Connection in Infisical + + + + 1. Navigate to the App Connections tab on the Organization Settings page. + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + 2. Select the **LDAP Connection** option. + ![Select LDAP Connection](/images/app-connections/ldap/select-ldap-connection.png) + + 3. Select the **Simple Bind** method option and provide the details obtained from the previous section and press **Connect to Provider**. + ![Create LDAP Connection](/images/app-connections/ldap/create-simple-bind-method.png) + + 4. Your **LDAP Connection** is now available for use. + ![Assume Role LDAP Connection](/images/app-connections/ldap/simple-bind-connection.png) + + + To create an LDAP Connection, make an API request to the [Create LDAP + Connection](/api-reference/endpoints/app-connections/ldap/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/ldap \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-ldap-connection", + "method": "simple-bind", + "credentials": { + "provider": "active-directory", + "url": "ldaps://domain-or-ip:636", + "dn": "CN=John,CN=Users,DC=example,DC=com", + "password": "my-strong-password", + "sslRejectUnauthorized": true, + "sslCertificate": "..." + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-ldap-connection", + "version": 1, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "app": "ldap", + "method": "simple-bind", + "credentials": { + "provider": "active-directory", + "url": "ldaps://domain-or-ip:636", + "dn": "CN=John,CN=Users,DC=example,DC=com", + "sslRejectUnauthorized": true, + "sslCertificate": "..." + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index ee07fb048..053945f4a 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -179,8 +179,9 @@ "pages": [ "documentation/platform/secret-rotation/overview", "documentation/platform/secret-rotation/auth0-client-secret", - "documentation/platform/secret-rotation/postgres-credentials", - "documentation/platform/secret-rotation/mssql-credentials" + "documentation/platform/secret-rotation/ldap-password", + "documentation/platform/secret-rotation/mssql-credentials", + "documentation/platform/secret-rotation/postgres-credentials" ] }, { @@ -424,6 +425,7 @@ "integrations/app-connections/gcp", "integrations/app-connections/github", "integrations/app-connections/humanitec", + "integrations/app-connections/ldap", "integrations/app-connections/mssql", "integrations/app-connections/postgres", "integrations/app-connections/terraform-cloud", @@ -861,6 +863,19 @@ "api-reference/endpoints/secret-rotations/auth0-client-secret/update" ] }, + { + "group": "LDAP Password", + "pages": [ + "api-reference/endpoints/secret-rotations/ldap-password/create", + "api-reference/endpoints/secret-rotations/ldap-password/delete", + "api-reference/endpoints/secret-rotations/ldap-password/get-by-id", + "api-reference/endpoints/secret-rotations/ldap-password/get-by-name", + "api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/ldap-password/list", + "api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets", + "api-reference/endpoints/secret-rotations/ldap-password/update" + ] + }, { "group": "Microsoft SQL Server Credentials", "pages": [ @@ -1013,6 +1028,18 @@ "api-reference/endpoints/app-connections/humanitec/delete" ] }, + { + "group": "LDAP", + "pages": [ + "api-reference/endpoints/app-connections/ldap/list", + "api-reference/endpoints/app-connections/ldap/available", + "api-reference/endpoints/app-connections/ldap/get-by-id", + "api-reference/endpoints/app-connections/ldap/get-by-name", + "api-reference/endpoints/app-connections/ldap/create", + "api-reference/endpoints/app-connections/ldap/update", + "api-reference/endpoints/app-connections/ldap/delete" + ] + }, { "group": "Microsoft SQL Server", "pages": [ diff --git a/frontend/public/images/integrations/LDAP.png b/frontend/public/images/integrations/LDAP.png new file mode 100644 index 000000000..4cf290176 Binary files /dev/null and b/frontend/public/images/integrations/LDAP.png differ diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx index d2340f42d..420889d2b 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx @@ -1,8 +1,6 @@ -import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay"; -import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2"; import { TAuth0ClientSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/auth0-client-secret-rotation"; -import { ViewRotationGeneratedCredentialsDisplay } from "./shared"; +import { CredentialDisplay, ViewRotationGeneratedCredentialsDisplay } from "./shared"; type Props = { generatedCredentialsResponse: TAuth0ClientSecretRotationGeneratedCredentialsResponse; @@ -17,40 +15,23 @@ export const ViewAuth0ClientSecretRotationGeneratedCredentials = ({ const inactiveCredentials = generatedCredentials[inactiveIndex]; return ( - <> - - {activeCredentials?.clientId} - - {activeCredentials?.clientSecret} - - - } - inactiveCredentials={ - <> - {inactiveCredentials?.clientId} - - {inactiveCredentials?.clientSecret} - - - } - /> - -

- Due to how Auth0 client secrets are rotated, retired credentials will not be able to - authenticate with Auth0 during their{" "} - - inactive period - - . This is a limitation of the Auth0 platform and cannot be rectified by Infisical. -

-
- + + {activeCredentials?.clientId} + + {activeCredentials?.clientSecret} + + + } + inactiveCredentials={ + <> + {inactiveCredentials?.clientId} + + {inactiveCredentials?.clientSecret} + + + } + /> ); }; diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx new file mode 100644 index 000000000..238dabea3 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx @@ -0,0 +1,41 @@ +import { TLdapPasswordRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation"; + +import { CredentialDisplay, ViewRotationGeneratedCredentialsDisplay } from "./shared"; + +type Props = { + generatedCredentialsResponse: TLdapPasswordRotationGeneratedCredentialsResponse; +}; + +export const ViewLdapPasswordRotationGeneratedCredentials = ({ + generatedCredentialsResponse: { generatedCredentials, activeIndex } +}: Props) => { + const inactiveIndex = activeIndex === 0 ? 1 : 0; + + const activeCredentials = generatedCredentials[activeIndex]; + const inactiveCredentials = generatedCredentials[inactiveIndex]; + + return ( + + + {activeCredentials?.dn} + + + {activeCredentials?.password} + + + } + inactiveCredentials={ + <> + + {inactiveCredentials?.dn} + + + {inactiveCredentials?.password} + + + } + /> + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx index 7d594b0f5..b0f05cd23 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx @@ -4,8 +4,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { format } from "date-fns"; import { ViewAuth0ClientSecretRotationGeneratedCredentials } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials"; +import { ViewLdapPasswordRotationGeneratedCredentials } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials"; import { Modal, ModalContent, Spinner } from "@app/components/v2"; -import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2"; +import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2"; +import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; +import { + IS_ROTATION_DUAL_CREDENTIALS, + SECRET_ROTATION_CONNECTION_MAP, + SECRET_ROTATION_MAP +} from "@app/helpers/secretRotationsV2"; import { SecretRotation, TSecretRotationV2, @@ -67,13 +74,39 @@ const Content = ({ secretRotation }: ContentProps) => { /> ); break; + case SecretRotation.LdapPassword: + Component = ( + + ); + break; default: throw new Error("Unhandled View Generated Credential Rotation Type"); } + const appName = APP_CONNECTION_MAP[SECRET_ROTATION_CONNECTION_MAP[type]].name; + return (
{Component} + {!IS_ROTATION_DUAL_CREDENTIALS[type] && ( + +

+ Due to {SECRET_ROTATION_MAP[type].name} Rotations utilizing a single credential set, + retired credentials will not be able to authenticate with {appName} during their{" "} + + inactive period + + . This is a limitation of {appName} and cannot be rectified by Infisical. +

+
+ )} {nextRotationAt && (
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx new file mode 100644 index 000000000..135abff4e --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx @@ -0,0 +1,201 @@ +import { Controller, useFormContext } from "react-hook-form"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { DEFAULT_PASSWORD_REQUIREMENTS } from "@app/components/secret-rotations-v2/forms/schemas/shared"; +import { FormControl, Input, Tooltip } from "@app/components/v2"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const LdapPasswordRotationParametersFields = () => { + const { control } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.LdapPassword; + } + >(); + + return ( + <> + ( + + Ensure that your connection has the{" "} + read_clients permission and the + application exists in the connection's audience. + + } + > +
+ Don't see the application you're looking for?{" "} + +
+ + } + > + {/* client.id === value) ?? null} + onChange={(option) => { + onChange((option as SingleValue)?.id ?? null); + }} + options={clients} + placeholder="Select an application..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> */} + +
+ )} + /> +
+
+ Password Requirements +
+
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> +
+
+ + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx index 444510e1e..d5847bb20 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx @@ -4,12 +4,14 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { TSecretRotationV2Form } from "../schemas"; import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRotationParametersFields"; +import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields"; import { SqlCredentialsRotationParametersFields } from "./shared"; const COMPONENT_MAP: Record = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields, - [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields + [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields, + [SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields }; export const SecretRotationV2ParametersFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/LdapPasswordRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/LdapPasswordRotationReviewFields.tsx new file mode 100644 index 000000000..1ffcad139 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/LdapPasswordRotationReviewFields.tsx @@ -0,0 +1,29 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +import { SecretRotationReviewSection } from "./shared"; + +export const LdapPasswordRotationReviewFields = () => { + const { watch } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.LdapPassword; + } + >(); + + const [parameters, { dn, password }] = watch(["parameters", "secretsMapping"]); + + return ( + <> + + {parameters.dn} + + + {dn} + {password} + + + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx index 4fb3b6d24..4961c38b4 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx @@ -7,12 +7,14 @@ import { getRotateAtLocal } from "@app/helpers/secretRotationsV2"; import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotationReviewFields"; +import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields"; import { SqlCredentialsRotationReviewFields } from "./shared"; const COMPONENT_MAP: Record = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields, - [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields + [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields, + [SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields }; export const SecretRotationV2ReviewFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx new file mode 100644 index 000000000..01d2e0d74 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx @@ -0,0 +1,58 @@ +import { Controller, useFormContext } from "react-hook-form"; + +import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; +import { FormControl, Input } from "@app/components/v2"; +import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2"; + +import { SecretsMappingTable } from "./shared"; + +export const LdapPasswordRotationSecretsMappingFields = () => { + const { control } = useFormContext< + TSecretRotationV2Form & { + type: SecretRotation.LdapPassword; + } + >(); + + const { rotationOption } = useSecretRotationV2Option(SecretRotation.LdapPassword); + + const items = [ + { + name: "DN", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.dn" + /> + ) + }, + { + name: "Password", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.password" + /> + ) + } + ]; + + return ; +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx index 58277d593..5f68cba1c 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx @@ -4,12 +4,14 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { TSecretRotationV2Form } from "../schemas"; import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecretRotationSecretsMappingFields"; +import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields"; import { SqlCredentialsRotationSecretsMappingFields } from "./shared"; const COMPONENT_MAP: Record = { [SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields, - [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields + [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields, + [SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields }; export const SecretRotationV2SecretsMappingFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts index 295e199fe..58d87c5e2 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts @@ -1,13 +1,15 @@ import { z } from "zod"; import { Auth0ClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/auth0-client-secret-rotation-schema"; +import { LdapPasswordRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema"; import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema"; import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema"; const SecretRotationUnionSchema = z.discriminatedUnion("type", [ PostgresCredentialsRotationSchema, MsSqlCredentialsRotationSchema, - Auth0ClientSecretRotationSchema + Auth0ClientSecretRotationSchema, + LdapPasswordRotationSchema ]); export const SecretRotationV2FormSchema = SecretRotationUnionSchema; diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema.ts new file mode 100644 index 000000000..8cebc199f --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema"; +import { PasswordRequirementsSchema } from "@app/components/secret-rotations-v2/forms/schemas/shared"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const LdapPasswordRotationSchema = z + .object({ + type: z.literal(SecretRotation.LdapPassword), + parameters: z.object({ + dn: z.string().trim().min(1, "Distinguished Name (DN) required"), + passwordRequirements: PasswordRequirementsSchema.optional() + }), + secretsMapping: z.object({ + dn: z.string().trim().min(1, "Distinguished Name (DN) required"), + password: z.string().trim().min(1, "Password required") + }) + }) + .merge(BaseSecretRotationSchema); diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts index 44b4c194f..284b705e4 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts @@ -1 +1,2 @@ +export * from "./password-requirements-schema"; export * from "./sql-credentials-rotation-schema"; diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/shared/password-requirements-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/password-requirements-schema.ts new file mode 100644 index 000000000..640ecdd88 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/password-requirements-schema.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +export const PasswordRequirementsSchema = z + .object({ + length: z + .number() + .min(1, "Password length must be a positive number") + .max(250, "Password length must be less than 250"), + required: z.object({ + digits: z.number().min(0, "Digit count must be non-negative"), + lowercase: z.number().min(0, "Lowercase count must be non-negative"), + uppercase: z.number().min(0, "Uppercase count must be non-negative"), + symbols: z.number().min(0, "Symbol count must be non-negative") + }), + allowedSymbols: z + .string() + .optional() + .transform((value) => value ?? "-_.~!*") + }) + .refine( + (data) => { + const total = Object.values(data.required).reduce((sum, count) => sum + count, 0); + return total <= data.length; + }, + { message: "Sum of required characters cannot exceed the total length", path: ["length"] } + ); + +export const DEFAULT_PASSWORD_REQUIREMENTS = { + length: 48, + required: { + lowercase: 1, + uppercase: 1, + digits: 1, + symbols: 0 + }, + allowedSymbols: "-_.~!*" +}; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index c94bd6648..ef8329b97 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -1,5 +1,12 @@ import { faGithub } from "@fortawesome/free-brands-svg-icons"; -import { faKey, faLock, faPassport, faServer, faUser } from "@fortawesome/free-solid-svg-icons"; +import { + faKey, + faLink, + faLock, + faPassport, + faServer, + faUser +} from "@fortawesome/free-solid-svg-icons"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { @@ -12,6 +19,7 @@ import { GcpConnectionMethod, GitHubConnectionMethod, HumanitecConnectionMethod, + LdapConnectionMethod, MsSqlConnectionMethod, PostgresConnectionMethod, TAppConnection, @@ -43,7 +51,8 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.MsSql]: { name: "Microsoft SQL Server", image: "MsSql.png" }, [AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" }, [AppConnection.Windmill]: { name: "Windmill", image: "Windmill.png" }, - [AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 } + [AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 }, + [AppConnection.Ldap]: { name: "LDAP", image: "LDAP.png", size: 65 } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -75,6 +84,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) return { name: "Access Token", icon: faKey }; case Auth0ConnectionMethod.ClientCredentials: return { name: "Client Credentials", icon: faServer }; + case LdapConnectionMethod.SimpleBind: + return { name: "Simple Bind", icon: faLink }; default: throw new Error(`Unhandled App Connection Method: ${method}`); } diff --git a/frontend/src/helpers/secretRotationsV2.ts b/frontend/src/helpers/secretRotationsV2.ts index 1a57d37cd..73b6e141c 100644 --- a/frontend/src/helpers/secretRotationsV2.ts +++ b/frontend/src/helpers/secretRotationsV2.ts @@ -19,20 +19,27 @@ export const SECRET_ROTATION_MAP: Record< name: "Auth0 Client Secret", image: "Auth0.png", size: 35 + }, + [SecretRotation.LdapPassword]: { + name: "LDAP Password", + image: "LDAP.png", + size: 65 } }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { [SecretRotation.PostgresCredentials]: AppConnection.Postgres, [SecretRotation.MsSqlCredentials]: AppConnection.MsSql, - [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0 + [SecretRotation.Auth0ClientSecret]: AppConnection.Auth0, + [SecretRotation.LdapPassword]: AppConnection.Ldap }; // if a rotation can potentially have downtime due to rotating a single credential set this to false export const IS_ROTATION_DUAL_CREDENTIALS: Record = { [SecretRotation.PostgresCredentials]: true, [SecretRotation.MsSqlCredentials]: true, - [SecretRotation.Auth0ClientSecret]: false + [SecretRotation.Auth0ClientSecret]: false, + [SecretRotation.LdapPassword]: false }; export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => { diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 704b200bb..419f24a07 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -12,5 +12,6 @@ export enum AppConnection { MsSql = "mssql", Camunda = "camunda", Windmill = "windmill", - Auth0 = "auth0" + Auth0 = "auth0", + Ldap = "ldap" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index ae57fbda2..b1f19c7c2 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -67,6 +67,10 @@ export type TAuth0ConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Auth0; }; +export type TLdapConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Ldap; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -98,4 +102,5 @@ export type TAppConnectionOptionMap = { [AppConnection.Camunda]: TCamundaConnectionOption; [AppConnection.Windmill]: TWindmillConnectionOption; [AppConnection.Auth0]: TAuth0ConnectionOption; + [AppConnection.Ldap]: TLdapConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 29e82bff6..dab5834e4 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -9,6 +9,7 @@ import { TDatabricksConnection } from "./databricks-connection"; import { TGcpConnection } from "./gcp-connection"; import { TGitHubConnection } from "./github-connection"; import { THumanitecConnection } from "./humanitec-connection"; +import { TLdapConnection } from "./ldap-connection"; import { TMsSqlConnection } from "./mssql-connection"; import { TPostgresConnection } from "./postgres-connection"; import { TTerraformCloudConnection } from "./terraform-cloud-connection"; @@ -24,6 +25,7 @@ export * from "./databricks-connection"; export * from "./gcp-connection"; export * from "./github-connection"; export * from "./humanitec-connection"; +export * from "./ldap-connection"; export * from "./mssql-connection"; export * from "./postgres-connection"; export * from "./terraform-cloud-connection"; @@ -44,7 +46,8 @@ export type TAppConnection = | TMsSqlConnection | TCamundaConnection | TWindmillConnection - | TAuth0Connection; + | TAuth0Connection + | TLdapConnection; export type TAvailableAppConnection = Pick; @@ -86,4 +89,5 @@ export type TAppConnectionMap = { [AppConnection.Camunda]: TCamundaConnection; [AppConnection.Windmill]: TWindmillConnection; [AppConnection.Auth0]: TAuth0Connection; + [AppConnection.Ldap]: TLdapConnection; }; diff --git a/frontend/src/hooks/api/appConnections/types/ldap-connection.ts b/frontend/src/hooks/api/appConnections/types/ldap-connection.ts new file mode 100644 index 000000000..4ce61c4ba --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/ldap-connection.ts @@ -0,0 +1,21 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum LdapConnectionMethod { + SimpleBind = "simple-bind" +} + +export enum LdapConnectionProvider { + ActiveDirectory = "active-directory" +} + +export type TLdapConnection = TRootAppConnection & { app: AppConnection.Ldap } & { + method: LdapConnectionMethod.SimpleBind; + credentials: { + provider: LdapConnectionProvider; + url: string; + dn: string; + sslRejectUnauthorized?: boolean; + sslCertificate?: string; + }; +}; diff --git a/frontend/src/hooks/api/secretRotationsV2/enums.ts b/frontend/src/hooks/api/secretRotationsV2/enums.ts index d43cacb3a..a094af635 100644 --- a/frontend/src/hooks/api/secretRotationsV2/enums.ts +++ b/frontend/src/hooks/api/secretRotationsV2/enums.ts @@ -1,7 +1,8 @@ export enum SecretRotation { PostgresCredentials = "postgres-credentials", MsSqlCredentials = "mssql-credentials", - Auth0ClientSecret = "auth0-client-secret" + Auth0ClientSecret = "auth0-client-secret", + LdapPassword = "ldap-password" } export enum SecretRotationStatus { diff --git a/frontend/src/hooks/api/secretRotationsV2/types/index.ts b/frontend/src/hooks/api/secretRotationsV2/types/index.ts index 96d568d74..6fbe47911 100644 --- a/frontend/src/hooks/api/secretRotationsV2/types/index.ts +++ b/frontend/src/hooks/api/secretRotationsV2/types/index.ts @@ -4,6 +4,11 @@ import { TAuth0ClientSecretRotationGeneratedCredentialsResponse, TAuth0ClientSecretRotationOption } from "@app/hooks/api/secretRotationsV2/types/auth0-client-secret-rotation"; +import { + TLdapPasswordRotation, + TLdapPasswordRotationGeneratedCredentialsResponse, + TLdapPasswordRotationOption +} from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation"; import { TMsSqlCredentialsRotation, TMsSqlCredentialsRotationGeneratedCredentialsResponse @@ -20,13 +25,15 @@ export type TSecretRotationV2 = ( | TPostgresCredentialsRotation | TMsSqlCredentialsRotation | TAuth0ClientSecretRotation + | TLdapPasswordRotation ) & { secrets: (SecretV3RawSanitized | null)[]; }; export type TSecretRotationV2Option = | TSqlCredentialsRotationOption - | TAuth0ClientSecretRotationOption; + | TAuth0ClientSecretRotationOption + | TLdapPasswordRotationOption; export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] }; @@ -35,7 +42,8 @@ export type TSecretRotationV2Response = { secretRotation: TSecretRotationV2 }; export type TViewSecretRotationGeneratedCredentialsResponse = | TPostgresCredentialsRotationGeneratedCredentialsResponse | TMsSqlCredentialsRotationGeneratedCredentialsResponse - | TAuth0ClientSecretRotationGeneratedCredentialsResponse; + | TAuth0ClientSecretRotationGeneratedCredentialsResponse + | TLdapPasswordRotationGeneratedCredentialsResponse; export type TCreateSecretRotationV2DTO = DiscriminativePick< TSecretRotationV2, @@ -82,10 +90,12 @@ export type TSecretRotationOptionMap = { [SecretRotation.PostgresCredentials]: TSqlCredentialsRotationOption; [SecretRotation.MsSqlCredentials]: TSqlCredentialsRotationOption; [SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationOption; + [SecretRotation.LdapPassword]: TLdapPasswordRotationOption; }; export type TSecretRotationGeneratedCredentialsResponseMap = { [SecretRotation.PostgresCredentials]: TPostgresCredentialsRotationGeneratedCredentialsResponse; [SecretRotation.MsSqlCredentials]: TMsSqlCredentialsRotationGeneratedCredentialsResponse; [SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationGeneratedCredentialsResponse; + [SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse; }; diff --git a/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts new file mode 100644 index 000000000..b5d1c33d9 --- /dev/null +++ b/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts @@ -0,0 +1,37 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; +import { + TSecretRotationV2Base, + TSecretRotationV2GeneratedCredentialsResponseBase +} from "@app/hooks/api/secretRotationsV2/types/shared"; + +export type TLdapPasswordRotation = TSecretRotationV2Base & { + type: SecretRotation.LdapPassword; + parameters: { + dn: string; + }; + secretsMapping: { + dn: string; + password: string; + }; +}; + +export type TLdapPasswordRotationGeneratedCredentials = { + dn: string; + password: string; +}; + +export type TLdapPasswordRotationGeneratedCredentialsResponse = + TSecretRotationV2GeneratedCredentialsResponseBase< + SecretRotation.LdapPassword, + TLdapPasswordRotationGeneratedCredentials + >; + +export type TLdapPasswordRotationOption = { + name: string; + type: SecretRotation.LdapPassword; + connection: AppConnection.Ldap; + template: { + secretsMapping: TLdapPasswordRotation["secretsMapping"]; + }; +}; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 05a7eebbc..e4c86c03e 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -18,6 +18,7 @@ import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; +import { LdapConnectionForm } from "./LdapConnectionFields"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm"; import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; @@ -89,6 +90,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.Ldap: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -153,6 +156,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.Ldap: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionFields.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionFields.tsx new file mode 100644 index 000000000..ccc433895 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionFields.tsx @@ -0,0 +1,319 @@ +import { useState } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +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 { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem, + Switch, + TextArea, + Tooltip +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { + LdapConnectionMethod, + LdapConnectionProvider, + TLdapConnection +} from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TLdapConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Ldap) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(LdapConnectionMethod.SimpleBind), + credentials: z.object({ + provider: z.nativeEnum(LdapConnectionProvider), + url: z.string().url().trim().min(1, "LDAP URL required"), + dn: z.string().trim().min(1, "Distinguished Name (DN) required"), + password: z.string().trim().min(1, "Password required"), + sslRejectUnauthorized: z.boolean(), + sslCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() + }) + }) +]); + +type FormData = z.infer; + +export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Ldap, + method: LdapConnectionMethod.SimpleBind, + credentials: { + provider: LdapConnectionProvider.ActiveDirectory, + url: "", + dn: "", + password: "", + sslRejectUnauthorized: true, + sslCertificate: undefined + } + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty }, + watch + } = form; + + const selectedProvider = watch("credentials.provider"); + const sslEnabled = watch("credentials.url").startsWith("ldaps://"); + + return ( + +
{ + setSelectedTabIndex(0); + handleSubmit(onSubmit)(e); + }} + > + {!isUpdate && } +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + + + `w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + Configuration + + + `w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + SSL ({sslEnabled ? "Enabled" : "Disabled"}) + + + {selectedTabIndex === 1 && ( +
Requires ldaps:// URL
+ )} + + + ( + + + + )} + /> +
+ ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+
+ + ( + +