diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts index 0fd01b753..07cf97a7e 100644 --- a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts @@ -1,4 +1,4 @@ -import ldap from "ldapjs"; +import ldap, { Client, SearchOptions } from "ldapjs"; import { TRotationFactory, @@ -8,26 +8,73 @@ import { TRotationFactoryRotateCredentials } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; import { logger } from "@app/lib/logger"; +import { DistinguishedNameRegex } from "@app/lib/regex"; import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap"; import { generatePassword } from "../shared/utils"; import { + LdapPasswordRotationMethod, TLdapPasswordRotationGeneratedCredentials, + TLdapPasswordRotationInput, TLdapPasswordRotationWithConnection } from "./ldap-password-rotation-types"; const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le"); +const getDN = async (dn: string, client: Client): Promise => { + if (DistinguishedNameRegex.test(dn)) return dn; + + const opts: SearchOptions = { + filter: `(userPrincipalName=${dn})`, + scope: "sub", + attributes: ["dn"] + }; + + const base = dn + .split("@")[1] + .split(".") + .map((dc) => `dc=${dc}`) + .join(","); + + return new Promise((resolve, reject) => { + // Perform the search + client.search(base, opts, (err, res) => { + if (err) { + logger.error(err, "LDAP Failed to get DN"); + reject(new Error(`Provider Resolve DN Error: ${err.message}`)); + } + + let userDn: string | null; + + res.on("searchEntry", (entry) => { + userDn = entry.objectName; + }); + + res.on("error", (error) => { + logger.error(error, "LDAP Failed to get DN"); + reject(new Error(`Provider Resolve DN Error: ${error.message}`)); + }); + + res.on("end", () => { + if (userDn) { + resolve(userDn); + } else { + reject(new Error(`Unable to resolve DN for ${dn}.`)); + } + }); + }); + }); +}; + export const ldapPasswordRotationFactory: TRotationFactory< TLdapPasswordRotationWithConnection, - TLdapPasswordRotationGeneratedCredentials + TLdapPasswordRotationGeneratedCredentials, + TLdapPasswordRotationInput["temporaryParameters"] > = (secretRotation, appConnectionDAL, kmsService) => { - const { - connection, - parameters: { dn, passwordRequirements }, - secretsMapping - } = secretRotation; + const { connection, parameters, secretsMapping, activeIndex } = secretRotation; + + const { dn, passwordRequirements } = parameters; const $verifyCredentials = async (credentials: Pick) => { try { @@ -40,13 +87,21 @@ export const ldapPasswordRotationFactory: TRotationFactory< } }; - const $rotatePassword = async () => { + const $rotatePassword = async (currentPassword?: string) => { 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 client = await getLdapConnectionClient( + currentPassword + ? { + ...credentials, + password: currentPassword, + dn + } + : credentials + ); + const isConnectionRotation = credentials.dn === dn; const password = generatePassword(passwordRequirements); @@ -58,8 +113,8 @@ export const ldapPasswordRotationFactory: TRotationFactory< const encodedPassword = getEncodedPassword(password); // service account vs personal password rotation require different changes - if (isPersonalRotation) { - const currentEncodedPassword = getEncodedPassword(credentials.password); + if (isConnectionRotation || currentPassword) { + const currentEncodedPassword = getEncodedPassword(currentPassword || credentials.password); changes = [ new ldap.Change({ @@ -93,8 +148,9 @@ export const ldapPasswordRotationFactory: TRotationFactory< } try { + const userDn = await getDN(dn, client); await new Promise((resolve, reject) => { - client.modify(dn, changes, (err) => { + client.modify(userDn, changes, (err) => { if (err) { logger.error(err, "LDAP Password Rotation Failed"); reject(new Error(`Provider Modify Error: ${err.message}`)); @@ -110,7 +166,7 @@ export const ldapPasswordRotationFactory: TRotationFactory< await $verifyCredentials({ dn, password }); - if (isPersonalRotation) { + if (isConnectionRotation) { const updatedCredentials: TLdapConnection["credentials"] = { ...credentials, password @@ -128,29 +184,41 @@ export const ldapPasswordRotationFactory: TRotationFactory< return { dn, password }; }; - const issueCredentials: TRotationFactoryIssueCredentials = async ( - callback - ) => { - const credentials = await $rotatePassword(); + const issueCredentials: TRotationFactoryIssueCredentials< + TLdapPasswordRotationGeneratedCredentials, + TLdapPasswordRotationInput["temporaryParameters"] + > = async (callback, temporaryParameters) => { + const credentials = await $rotatePassword( + parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal + ? temporaryParameters?.password + : undefined + ); return callback(credentials); }; const revokeCredentials: TRotationFactoryRevokeCredentials = async ( - _, + credentialsToRevoke, callback ) => { + const currentPassword = credentialsToRevoke[activeIndex].password; + // we just rotate to a new password, essentially revoking old credentials - await $rotatePassword(); + await $rotatePassword( + parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal ? currentPassword : undefined + ); return callback(); }; const rotateCredentials: TRotationFactoryRotateCredentials = async ( _, - callback + callback, + activeCredentials ) => { - const credentials = await $rotatePassword(); + const credentials = await $rotatePassword( + parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal ? activeCredentials.password : undefined + ); return callback(credentials); }; 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 index e99569d9a..741cd3ce1 100644 --- 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 @@ -1,6 +1,6 @@ -import RE2 from "re2"; import { z } from "zod"; +import { LdapPasswordRotationMethod } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types"; import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; import { BaseCreateSecretRotationSchema, @@ -9,7 +9,7 @@ import { } 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 { DistinguishedNameRegex } from "@app/lib/regex"; +import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/lib/regex"; import { SecretNameSchema } from "@app/server/lib/schemas"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; @@ -26,10 +26,16 @@ const LdapPasswordRotationParametersSchema = z.object({ dn: z .string() .trim() - .regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com") - .min(1, "Distinguished Name (DN) Required") + .min(1, "DN/UPN required") + .refine((value) => DistinguishedNameRegex.test(value) || UserPrincipalNameRegex.test(value), { + message: "Invalid DN/UPN format" + }) .describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn), - passwordRequirements: PasswordRequirementsSchema.optional() + passwordRequirements: PasswordRequirementsSchema.optional(), + rotationMethod: z + .nativeEnum(LdapPasswordRotationMethod) + .optional() + .describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.rotationMethod) }); const LdapPasswordRotationSecretsMappingSchema = z.object({ @@ -50,10 +56,28 @@ export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotatio secretsMapping: LdapPasswordRotationSecretsMappingSchema }); -export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({ - parameters: LdapPasswordRotationParametersSchema, - secretsMapping: LdapPasswordRotationSecretsMappingSchema -}); +export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword) + .extend({ + parameters: LdapPasswordRotationParametersSchema, + secretsMapping: LdapPasswordRotationSecretsMappingSchema, + temporaryParameters: z + .object({ + password: z.string().min(1, "Password required").describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.password) + }) + .optional() + }) + .superRefine((val, ctx) => { + if ( + val.parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal && + !val.temporaryParameters?.password + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Password required", + path: ["temporaryParameters", "password"] + }); + } + }); export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({ parameters: LdapPasswordRotationParametersSchema.optional(), 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 index cb15b0734..86437cac5 100644 --- 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 @@ -9,6 +9,11 @@ import { LdapPasswordRotationSchema } from "./ldap-password-rotation-schemas"; +export enum LdapPasswordRotationMethod { + ConnectionPrincipal = "connection-principal", + TargetPrincipal = "target-principal" +} + export type TLdapPasswordRotation = z.infer; export type TLdapPasswordRotationInput = z.infer; 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 a25482c8c..1be7dc802 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 @@ -1,12 +1,13 @@ import { AxiosError } from "axios"; import { getConfig } from "@app/lib/config/env"; +import { BadRequestError } from "@app/lib/errors"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret"; import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret"; import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret"; -import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password"; +import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } 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"; @@ -15,7 +16,8 @@ import { TSecretRotationV2, TSecretRotationV2GeneratedCredentials, TSecretRotationV2ListItem, - TSecretRotationV2Raw + TSecretRotationV2Raw, + TUpdateSecretRotationV2DTO } from "./secret-rotation-v2-types"; const SECRET_ROTATION_LIST_OPTIONS: Record = { @@ -228,3 +230,30 @@ export const parseRotationErrorMessage = (err: unknown): string => { ? errorMessage : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`; }; + +function haveUnequalProperties(obj1: T, obj2: T, properties: (keyof T)[]): boolean { + return properties.some((prop) => obj1[prop] !== obj2[prop]); +} + +export const throwOnImmutableParameterUpdate = ( + updatePayload: TUpdateSecretRotationV2DTO, + secretRotation: TSecretRotationV2Raw +) => { + if (!updatePayload.parameters) return; + + switch (updatePayload.type) { + case SecretRotation.LdapPassword: + if ( + haveUnequalProperties( + updatePayload.parameters as TLdapPasswordRotation["parameters"], + secretRotation.parameters as TLdapPasswordRotation["parameters"], + ["rotationMethod", "dn"] + ) + ) { + throw new BadRequestError({ message: "Cannot update rotation method or DN" }); + } + break; + default: + // do nothing + } +}; 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 69743f133..352c99b2c 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 @@ -25,7 +25,8 @@ import { getNextUtcRotationInterval, getSecretRotationRotateSecretJobOptions, listSecretRotationOptions, - parseRotationErrorMessage + parseRotationErrorMessage, + throwOnImmutableParameterUpdate } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns"; import { SECRET_ROTATION_CONNECTION_MAP, @@ -46,6 +47,7 @@ import { TSecretRotationV2, TSecretRotationV2GeneratedCredentials, TSecretRotationV2Raw, + TSecretRotationV2TemporaryParameters, TSecretRotationV2WithConnection, TUpdateSecretRotationV2DTO } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; @@ -112,7 +114,8 @@ const MAX_GENERATED_CREDENTIALS_LENGTH = 2; type TRotationFactoryImplementation = TRotationFactory< TSecretRotationV2WithConnection, - TSecretRotationV2GeneratedCredentials + TSecretRotationV2GeneratedCredentials, + TSecretRotationV2TemporaryParameters >; const SECRET_ROTATION_FACTORY_MAP: Record = { [SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, @@ -400,6 +403,7 @@ export const secretRotationV2ServiceFactory = ({ environment, rotateAtUtc = { hours: 0, minutes: 0 }, secretsMapping, + temporaryParameters, ...payload }: TCreateSecretRotationV2DTO, actor: OrgServiceActor @@ -546,7 +550,7 @@ export const secretRotationV2ServiceFactory = ({ return createdRotation; }); - }); + }, temporaryParameters); await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); await snapshotService.performSnapshot(folder.id); @@ -585,10 +589,7 @@ export const secretRotationV2ServiceFactory = ({ } }; - const updateSecretRotation = async ( - { type, rotationId, ...payload }: TUpdateSecretRotationV2DTO, - actor: OrgServiceActor - ) => { + const updateSecretRotation = async (dto: TUpdateSecretRotationV2DTO, actor: OrgServiceActor) => { const plan = await licenseService.getPlan(actor.orgId); if (!plan.secretRotation) @@ -596,6 +597,8 @@ export const secretRotationV2ServiceFactory = ({ message: "Failed to update secret rotation due to plan restriction. Upgrade plan to update secret rotations." }); + const { type, rotationId, ...payload } = dto; + const secretRotation = await secretRotationV2DAL.findById(rotationId); if (!secretRotation) @@ -603,6 +606,8 @@ export const secretRotationV2ServiceFactory = ({ message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}` }); + throwOnImmutableParameterUpdate(dto, secretRotation); + const { folder, environment, projectId, folderId, connection } = secretRotation; const secretsMapping = secretRotation.secretsMapping as TSecretRotationV2["secretsMapping"]; @@ -877,6 +882,7 @@ export const secretRotationV2ServiceFactory = ({ const inactiveIndex = (activeIndex + 1) % MAX_GENERATED_CREDENTIALS_LENGTH; const inactiveCredentials = generatedCredentials[inactiveIndex]; + const activeCredentials = generatedCredentials[activeIndex]; const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation]( { @@ -887,73 +893,77 @@ export const secretRotationV2ServiceFactory = ({ kmsService ); - const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => { - const updatedCredentials = [...generatedCredentials]; - updatedCredentials[inactiveIndex] = newCredentials; + const updatedRotation = await rotationFactory.rotateCredentials( + inactiveCredentials, + async (newCredentials) => { + const updatedCredentials = [...generatedCredentials]; + updatedCredentials[inactiveIndex] = newCredentials; - const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({ - projectId, - generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials, - kmsService - }); - - return secretRotationV2DAL.transaction(async (tx) => { - const secretsPayload = rotationFactory.getSecretsPayload(newCredentials); - - const { encryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId + const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({ + projectId, + generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials, + kmsService }); - // update mapped secrets with new credential values - await fnSecretBulkUpdate({ - folderId, - orgId: connection.orgId, - tx, - inputSecrets: secretsPayload.map(({ key, value }) => ({ - filter: { - key, - folderId, - type: SecretType.Shared - }, - data: { - encryptedValue: encryptor({ - plainText: Buffer.from(value) - }).cipherTextBlob, - references: [] - } - })), - secretDAL: secretV2BridgeDAL, - secretVersionDAL: secretVersionV2BridgeDAL, - secretVersionTagDAL: secretVersionTagV2BridgeDAL, - secretTagDAL, - resourceMetadataDAL - }); + return secretRotationV2DAL.transaction(async (tx) => { + const secretsPayload = rotationFactory.getSecretsPayload(newCredentials); - const currentTime = new Date(); + const { encryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.SecretManager, + projectId + }); - return secretRotationV2DAL.updateById( - secretRotation.id, - { - encryptedGeneratedCredentials: encryptedUpdatedCredentials, - activeIndex: inactiveIndex, - isLastRotationManual: isManualRotation, - lastRotatedAt: currentTime, - lastRotationAttemptedAt: currentTime, - nextRotationAt: calculateNextRotationAt({ - ...(secretRotation as TSecretRotationV2), - rotationStatus: SecretRotationStatus.Success, + // update mapped secrets with new credential values + await fnSecretBulkUpdate({ + folderId, + orgId: connection.orgId, + tx, + inputSecrets: secretsPayload.map(({ key, value }) => ({ + filter: { + key, + folderId, + type: SecretType.Shared + }, + data: { + encryptedValue: encryptor({ + plainText: Buffer.from(value) + }).cipherTextBlob, + references: [] + } + })), + secretDAL: secretV2BridgeDAL, + secretVersionDAL: secretVersionV2BridgeDAL, + secretVersionTagDAL: secretVersionTagV2BridgeDAL, + secretTagDAL, + resourceMetadataDAL + }); + + const currentTime = new Date(); + + return secretRotationV2DAL.updateById( + secretRotation.id, + { + encryptedGeneratedCredentials: encryptedUpdatedCredentials, + activeIndex: inactiveIndex, + isLastRotationManual: isManualRotation, lastRotatedAt: currentTime, - isManualRotation - }), - rotationStatus: SecretRotationStatus.Success, - lastRotationJobId: jobId, - encryptedLastRotationMessage: null - }, - tx - ); - }); - }); + lastRotationAttemptedAt: currentTime, + nextRotationAt: calculateNextRotationAt({ + ...(secretRotation as TSecretRotationV2), + rotationStatus: SecretRotationStatus.Success, + lastRotatedAt: currentTime, + isManualRotation + }), + rotationStatus: SecretRotationStatus.Success, + lastRotationJobId: jobId, + encryptedLastRotationMessage: null + }, + tx + ); + }); + }, + activeCredentials + ); await auditLogService.createAuditLog({ ...(auditLogInfo ?? { 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 ab715c406..b72bfba31 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 @@ -87,6 +87,8 @@ export type TSecretRotationV2ListItem = | TLdapPasswordRotationListItem | TAwsIamUserSecretRotationListItem; +export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined; + export type TSecretRotationV2Raw = NonNullable>>; export type TListSecretRotationsV2ByProjectId = { @@ -120,6 +122,7 @@ export type TCreateSecretRotationV2DTO = Pick< environment: string; isAutoRotationEnabled?: boolean; rotateAtUtc?: TRotateAtUtc; + temporaryParameters?: TSecretRotationV2TemporaryParameters; }; export type TUpdateSecretRotationV2DTO = Partial< @@ -186,8 +189,12 @@ export type TSecretRotationSendNotificationJobPayload = { // transactional behavior. By passing in the rotation mutation, if this mutation fails we can roll back the // third party credential changes (when supported), preventing credentials getting out of sync -export type TRotationFactoryIssueCredentials = ( - callback: (newCredentials: T[number]) => Promise +export type TRotationFactoryIssueCredentials< + T extends TSecretRotationV2GeneratedCredentials, + P extends TSecretRotationV2TemporaryParameters = undefined +> = ( + callback: (newCredentials: T[number]) => Promise, + temporaryParameters?: P ) => Promise; export type TRotationFactoryRevokeCredentials = ( @@ -197,7 +204,8 @@ export type TRotationFactoryRevokeCredentials = ( credentialsToRevoke: T[number] | undefined, - callback: (newCredentials: T[number]) => Promise + callback: (newCredentials: T[number]) => Promise, + activeCredentials: T[number] ) => Promise; export type TRotationFactoryGetSecretsPayload = ( @@ -206,13 +214,14 @@ export type TRotationFactoryGetSecretsPayload = ( secretRotation: T, appConnectionDAL: Pick, kmsService: Pick ) => { - issueCredentials: TRotationFactoryIssueCredentials; + issueCredentials: TRotationFactoryIssueCredentials; revokeCredentials: TRotationFactoryRevokeCredentials; rotateCredentials: TRotationFactoryRotateCredentials; getSecretsPayload: TRotationFactoryGetSecretsPayload; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3fad36fac..1a30923d5 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2063,7 +2063,7 @@ export const AppConnections = { 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').", + dn: "The Distinguished Name (DN) or User Principal Name (UPN) 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.", @@ -2308,7 +2308,10 @@ export const SecretRotations = { clientId: "The client ID of the Azure Application to rotate the client secret for." }, LDAP_PASSWORD: { - dn: "The Distinguished Name (DN) of the principal to rotate the password for." + dn: "The Distinguished Name (DN) or User Principal Name (UPN) of the principal to rotate the password for.", + rotationMethod: + 'Whether the rotation should be performed by the LDAP "connection-principal" or the "target-principal" (defaults to \'connection-principal\').', + password: 'The password of the provided principal if "parameters.rotationMethod" is set to "target-principal".' }, GENERAL: { PASSWORD_REQUIREMENTS: { @@ -2342,7 +2345,7 @@ export const SecretRotations = { 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.", + dn: "The name of the secret that the Distinguished Name (DN) or User Principal Name (UPN) of the principal will be mapped to.", password: "The name of the secret that the rotated password will be mapped to." }, AWS_IAM_USER_SECRET: { diff --git a/backend/src/lib/regex/index.ts b/backend/src/lib/regex/index.ts index 68ba7671d..be9430669 100644 --- a/backend/src/lib/regex/index.ts +++ b/backend/src/lib/regex/index.ts @@ -1,3 +1,11 @@ +import RE2 from "re2"; + export const DistinguishedNameRegex = // DN format, ie; CN=user,OU=users,DC=example,DC=com - /^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/; + new RE2( + /^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/ + ); + +export const UserPrincipalNameRegex = new RE2(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9._-]+\.[a-zA-Z]{2,}$/); + +export const LdapUrlRegex = new RE2(/^ldaps?:\/\//); diff --git a/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts index 91884b914..c4c94b4fc 100644 --- a/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts +++ b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts @@ -1,8 +1,7 @@ -import RE2 from "re2"; import { z } from "zod"; import { AppConnections } from "@app/lib/api-docs"; -import { DistinguishedNameRegex } from "@app/lib/regex"; +import { DistinguishedNameRegex, LdapUrlRegex, UserPrincipalNameRegex } from "@app/lib/regex"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { BaseAppConnectionSchema, @@ -14,17 +13,14 @@ 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") - .regex(new RE2(/^ldaps?:\/\//)) - .describe(AppConnections.CREDENTIALS.LDAP.url), + url: z.string().trim().min(1, "URL required").regex(LdapUrlRegex).describe(AppConnections.CREDENTIALS.LDAP.url), dn: z .string() .trim() - .regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com") - .min(1, "Distinguished Name (DN) required") + .min(1, "DN/UPN required") + .refine((value) => DistinguishedNameRegex.test(value) || UserPrincipalNameRegex.test(value), { + message: "Invalid DN/UPN format" + }) .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), diff --git a/docs/documentation/platform/secret-rotation/ldap-password.mdx b/docs/documentation/platform/secret-rotation/ldap-password.mdx index 103fe4656..feb3a664d 100644 --- a/docs/documentation/platform/secret-rotation/ldap-password.mdx +++ b/docs/documentation/platform/secret-rotation/ldap-password.mdx @@ -28,7 +28,7 @@ description: "Learn how to automatically rotate LDAP passwords." 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 Connection** - the connection that will perform the rotation of the configured principal's password. LDAP Password Rotations require an LDAP Connection that uses ldaps:// protocol. @@ -40,13 +40,20 @@ description: "Learn how to automatically rotate LDAP passwords." - 4. Specify the Distinguished Name (DN) of the principal whose password you want to rotate and configure the password requirements. Then click **Next**. + 4. Configure the required Parameters for your rotation. Then click **Next**. ![Rotation Parameters](/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png) + - **Rotation Method** - The method to use when rotating the target principal's password. + - **Connection Principal** - Infisical will use the LDAP Connection's binding principal to rotate the target principal's password. + - **Target Principal** - Infisical will bind with the target Principal to rotate their own password. + - **DN/UPN** - The Distinguished Name (DN), or User Principal Name (UPN) if supported, of the principal whose password you want to rotate. + - **Password** - The target principal's password (if **Rotation Method** is set to **Target Principal**). + - **Password Requirements** - The constraints to apply when generating new passwords. + 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. + - **DN/UPN** - the name of the secret that the principal's Distinguished Name (DN) or User Principal Name (UPN) 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**. @@ -85,6 +92,7 @@ description: "Learn how to automatically rotate LDAP passwords." "minutes": 0 }, "parameters": { + "rotationMethod": "connection-principal", "dn": "CN=John,CN=Users,DC=example,DC=com", "passwordRequirements": { "length": 48, @@ -154,6 +162,7 @@ description: "Learn how to automatically rotate LDAP passwords." "lastRotationMessage": null, "type": "ldap-password", "parameters": { + "rotationMethod": "connection-principal", "dn": "CN=John,CN=Users,DC=example,DC=com", "passwordRequirements": { "length": 48, 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 index 1725c4355..707736095 100644 Binary files a/docs/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png 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-parameters.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png index dfe723b06..8dbfb8ddf 100644 Binary files a/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png differ diff --git a/docs/integrations/app-connections/ldap.mdx b/docs/integrations/app-connections/ldap.mdx index 63c4bfed1..db0b596ce 100644 --- a/docs/integrations/app-connections/ldap.mdx +++ b/docs/integrations/app-connections/ldap.mdx @@ -10,7 +10,7 @@ Infisical supports the use of [Simple Binding](https://ldap.com/the-ldap-bind-op 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 DN/UPN** - The Distinguished Name (DN), or User Principal Name (UPN) if supported, 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 diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx index 238dabea3..9e1476118 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx @@ -18,9 +18,7 @@ export const ViewLdapPasswordRotationGeneratedCredentials = ({ - - {activeCredentials?.dn} - + {activeCredentials?.dn} {activeCredentials?.password} @@ -28,9 +26,7 @@ export const ViewLdapPasswordRotationGeneratedCredentials = ({ } inactiveCredentials={ <> - - {inactiveCredentials?.dn} - + {inactiveCredentials?.dn} {inactiveCredentials?.password} diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx index e1c74b420..d931d9d3c 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2Form.tsx @@ -48,7 +48,8 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TSecretRotationV2Fo "rotateAtUtc" ] }, - { name: "Parameters", key: "parameters", fields: ["parameters"] }, + // @ts-expect-error temporary parameters aren't present on all forms + { name: "Parameters", key: "parameters", fields: ["parameters", "temporaryParameters"] }, { name: "Mappings", key: "secretsMapping", fields: ["secretsMapping"] }, { name: "Details", key: "details", fields: ["name", "description"] }, { name: "Review", key: "review", fields: [] } @@ -75,7 +76,7 @@ export const SecretRotationV2Form = ({ const { rotationOption } = useSecretRotationV2Option(type); const formMethods = useForm({ - resolver: zodResolver(SecretRotationV2FormSchema), + resolver: zodResolver(SecretRotationV2FormSchema(Boolean(secretRotation))), defaultValues: secretRotation ? { ...secretRotation, diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx index 9c9d8329f..243c18369 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx @@ -2,40 +2,135 @@ import { Controller, useFormContext } from "react-hook-form"; 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 } from "@app/components/v2"; +import { FormControl, Input, Select, SelectItem } from "@app/components/v2"; import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; +import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation"; export const LdapPasswordRotationParametersFields = () => { - const { control } = useFormContext< + const { control, watch, setValue } = useFormContext< TSecretRotationV2Form & { type: SecretRotation.LdapPassword; } >(); + const [id, rotationMethod] = watch(["id", "parameters.rotationMethod"]); + const isUpdate = Boolean(id); + return ( <> ( + Determines how the rotation will be performed: +
    +
  • + Connection Principal - The Connection + principal will rotate the target principal's password. +
  • +
  • + Target Principal - The target principal + will rotate their own password. +
  • +
+ + } + tooltipClassName="max-w-sm" errorText={error?.message} - label="Distinguished Name (DN)" + isError={Boolean(error?.message)} + label="Rotation Method" + helperText={ + // eslint-disable-next-line no-nested-ternary + isUpdate + ? "Cannot be updated." + : value === LdapPasswordRotationMethod.ConnectionPrincipal + ? "The connection principal will rotate the target principal's password" + : "The target principal will rotate their own password" + } > - + onValueChange={(val) => { + setValue( + "temporaryParameters", + val === LdapPasswordRotationMethod.TargetPrincipal + ? { + password: "" + } + : undefined + ); + onChange(val); + }} + className="w-full border border-mineshaft-500 capitalize" + position="popper" + dropdownContainerClassName="max-w-none" + > + {Object.values(LdapPasswordRotationMethod).map((method) => { + return ( + + {method.replace("-", " ")} + + ); + })} +
)} /> +
+ ( + + + + )} + /> + {rotationMethod === LdapPasswordRotationMethod.TargetPrincipal && !isUpdate && ( + ( + + + + )} + /> + )} +
Password Requirements
-
+
{ label="Password Length" isError={Boolean(error)} errorText={error?.message} - helperText="The length of the password to generate" + tooltipText="The length of the password to generate" > { label="Digit Count" isError={Boolean(error)} errorText={error?.message} - helperText="Minimum number of digits" + tooltipText="Minimum number of digits" > { label="Lowercase Character Count" isError={Boolean(error)} errorText={error?.message} - helperText="Minimum number of lowercase characters" + tooltipText="Minimum number of lowercase characters" > { label="Uppercase Character Count" isError={Boolean(error)} errorText={error?.message} - helperText="Minimum number of uppercase characters" + tooltipText="Minimum number of uppercase characters" > { label="Symbol Count" isError={Boolean(error)} errorText={error?.message} - helperText="Minimum number of symbols" + tooltipText="Minimum number of symbols" > { label="Allowed Symbols" isError={Boolean(error)} errorText={error?.message} - helperText="Symbols to use in generated password" + tooltipText="Symbols to use in generated password" > { const [parameters, { dn, password }] = watch(["parameters", "secretsMapping"]); + const { passwordRequirements } = parameters; + return ( <> - {parameters.dn} + {parameters.dn} + {passwordRequirements && ( + + {passwordRequirements.length} + + {passwordRequirements.required.digits} + + + {passwordRequirements.required.lowercase} + + + {passwordRequirements.required.uppercase} + + + {passwordRequirements.required.symbols} + + + {passwordRequirements.allowedSymbols} + + + )} - {dn} + {dn} {password} diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/shared/SecretRotationReviewSection.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/shared/SecretRotationReviewSection.tsx index 7e2da4a07..fdff4af5c 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/shared/SecretRotationReviewSection.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/shared/SecretRotationReviewSection.tsx @@ -1,7 +1,7 @@ import { ReactNode } from "react"; type Props = { - label: "Parameters" | "Secrets Mapping"; + label: "Parameters" | "Secrets Mapping" | "Password Requirements"; children: ReactNode; }; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx index 01d2e0d74..0c5c90662 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx @@ -17,7 +17,7 @@ export const LdapPasswordRotationSecretsMappingFields = () => { const items = [ { - name: "DN", + name: "DN/UPN", input: ( ( 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 b0484ae67..b8564801f 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts @@ -6,16 +6,36 @@ import { AzureClientSecretRotationSchema } from "@app/components/secret-rotation 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"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; +import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation"; -const SecretRotationUnionSchema = z.discriminatedUnion("type", [ - Auth0ClientSecretRotationSchema, - AzureClientSecretRotationSchema, - PostgresCredentialsRotationSchema, - MsSqlCredentialsRotationSchema, - LdapPasswordRotationSchema, - AwsIamUserSecretRotationSchema -]); +export const SecretRotationV2FormSchema = (isUpdate: boolean) => + z + .intersection( + z.discriminatedUnion("type", [ + Auth0ClientSecretRotationSchema, + AzureClientSecretRotationSchema, + PostgresCredentialsRotationSchema, + MsSqlCredentialsRotationSchema, + LdapPasswordRotationSchema, + AwsIamUserSecretRotationSchema + ]), + z.object({ id: z.string().optional() }) + ) + .superRefine((val, ctx) => { + if (val.type !== SecretRotation.LdapPassword || isUpdate) return; -export const SecretRotationV2FormSchema = SecretRotationUnionSchema; + // this has to go on union or breaks discrimination + if ( + val.parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal && + !val.temporaryParameters?.password + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Password required", + path: ["temporaryParameters", "password"] + }); + } + }); -export type TSecretRotationV2Form = z.infer; +export type TSecretRotationV2Form = z.infer>; 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 index e18609f04..58e998ee7 100644 --- 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 @@ -2,8 +2,9 @@ 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 { DistinguishedNameRegex } from "@app/helpers/string"; +import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/helpers/string"; import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; +import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation"; export const LdapPasswordRotationSchema = z .object({ @@ -12,13 +13,24 @@ export const LdapPasswordRotationSchema = z dn: z .string() .trim() - .regex(DistinguishedNameRegex, "Invalid Distinguished Name format") - .min(1, "Distinguished Name (DN) required"), - passwordRequirements: PasswordRequirementsSchema.optional() + .min(1, "DN/UPN required") + .refine( + (value) => DistinguishedNameRegex.test(value) || UserPrincipalNameRegex.test(value), + { + message: "Invalid DN/UPN format" + } + ), + passwordRequirements: PasswordRequirementsSchema.optional(), + rotationMethod: z.nativeEnum(LdapPasswordRotationMethod).optional() }), secretsMapping: z.object({ - dn: z.string().trim().min(1, "Distinguished Name (DN) required"), + dn: z.string().trim().min(1, "DN/UPN required"), password: z.string().trim().min(1, "Password required") - }) + }), + temporaryParameters: z + .object({ + password: z.string().min(1, "Password required") + }) + .optional() }) .merge(BaseSecretRotationSchema); 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 index a02852ec8..1bab3b0bd 100644 --- 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 @@ -1,5 +1,7 @@ import { z } from "zod"; +export type TPasswordRequirements = z.infer; + export const PasswordRequirementsSchema = z .object({ length: z diff --git a/frontend/src/helpers/string.ts b/frontend/src/helpers/string.ts index ddd9fb7c9..ff12afced 100644 --- a/frontend/src/helpers/string.ts +++ b/frontend/src/helpers/string.ts @@ -15,3 +15,5 @@ export const isValidPath = (val: string): boolean => { export const DistinguishedNameRegex = /^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/; + +export const UserPrincipalNameRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; diff --git a/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts index b8d2ade2b..42a1116cf 100644 --- a/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts +++ b/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts @@ -1,3 +1,4 @@ +import { TPasswordRequirements } from "@app/components/secret-rotations-v2/forms/schemas/shared"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { @@ -5,10 +6,17 @@ import { TSecretRotationV2GeneratedCredentialsResponseBase } from "@app/hooks/api/secretRotationsV2/types/shared"; +export enum LdapPasswordRotationMethod { + ConnectionPrincipal = "connection-principal", + TargetPrincipal = "target-principal" +} + export type TLdapPasswordRotation = TSecretRotationV2Base & { type: SecretRotation.LdapPassword; parameters: { dn: string; + rotationMethod?: LdapPasswordRotationMethod; + passwordRequirements?: TPasswordRequirements; }; secretsMapping: { dn: string; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx index 7346f84af..a13a5c184 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx @@ -19,7 +19,7 @@ import { Tooltip } from "@app/components/v2"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; -import { DistinguishedNameRegex } from "@app/helpers/string"; +import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/helpers/string"; import { LdapConnectionMethod, LdapConnectionProvider, @@ -55,8 +55,13 @@ const formSchema = z.discriminatedUnion("method", [ dn: z .string() .trim() - .regex(DistinguishedNameRegex, "Invalid Distinguished Name format") - .min(1, "Distinguished Name (DN) required"), + .min(1, "DN/UPN required") + .refine( + (value) => DistinguishedNameRegex.test(value) || UserPrincipalNameRegex.test(value), + { + message: "Invalid DN/UPN format" + } + ), password: z.string().trim().min(1, "Password required"), sslRejectUnauthorized: z.boolean(), sslCertificate: z @@ -223,7 +228,7 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {