Compare commits

...

1 Commits

Author SHA1 Message Date
83772080ef feature: add suport for target principal self rotation 2025-05-16 13:14:42 -07:00
26 changed files with 471 additions and 157 deletions

View File

@ -1,4 +1,4 @@
import ldap from "ldapjs"; import ldap, { Client, SearchOptions } from "ldapjs";
import { import {
TRotationFactory, TRotationFactory,
@ -8,26 +8,73 @@ import {
TRotationFactoryRotateCredentials TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { DistinguishedNameRegex } from "@app/lib/regex";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap"; import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
import { generatePassword } from "../shared/utils"; import { generatePassword } from "../shared/utils";
import { import {
LdapPasswordRotationMethod,
TLdapPasswordRotationGeneratedCredentials, TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationInput,
TLdapPasswordRotationWithConnection TLdapPasswordRotationWithConnection
} from "./ldap-password-rotation-types"; } from "./ldap-password-rotation-types";
const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le"); const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le");
const getDN = async (dn: string, client: Client): Promise<string> => {
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< export const ldapPasswordRotationFactory: TRotationFactory<
TLdapPasswordRotationWithConnection, TLdapPasswordRotationWithConnection,
TLdapPasswordRotationGeneratedCredentials TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationInput["temporaryParameters"]
> = (secretRotation, appConnectionDAL, kmsService) => { > = (secretRotation, appConnectionDAL, kmsService) => {
const { const { connection, parameters, secretsMapping, activeIndex } = secretRotation;
connection,
parameters: { dn, passwordRequirements }, const { dn, passwordRequirements } = parameters;
secretsMapping
} = secretRotation;
const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => { const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => {
try { try {
@ -40,13 +87,21 @@ export const ldapPasswordRotationFactory: TRotationFactory<
} }
}; };
const $rotatePassword = async () => { const $rotatePassword = async (currentPassword?: string) => {
const { credentials, orgId } = connection; const { credentials, orgId } = connection;
if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection"); if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection");
const client = await getLdapConnectionClient(credentials); const client = await getLdapConnectionClient(
const isPersonalRotation = credentials.dn === dn; currentPassword
? {
...credentials,
password: currentPassword,
dn
}
: credentials
);
const isConnectionRotation = credentials.dn === dn;
const password = generatePassword(passwordRequirements); const password = generatePassword(passwordRequirements);
@ -58,8 +113,8 @@ export const ldapPasswordRotationFactory: TRotationFactory<
const encodedPassword = getEncodedPassword(password); const encodedPassword = getEncodedPassword(password);
// service account vs personal password rotation require different changes // service account vs personal password rotation require different changes
if (isPersonalRotation) { if (isConnectionRotation || currentPassword) {
const currentEncodedPassword = getEncodedPassword(credentials.password); const currentEncodedPassword = getEncodedPassword(currentPassword || credentials.password);
changes = [ changes = [
new ldap.Change({ new ldap.Change({
@ -93,8 +148,9 @@ export const ldapPasswordRotationFactory: TRotationFactory<
} }
try { try {
const userDn = await getDN(dn, client);
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
client.modify(dn, changes, (err) => { client.modify(userDn, changes, (err) => {
if (err) { if (err) {
logger.error(err, "LDAP Password Rotation Failed"); logger.error(err, "LDAP Password Rotation Failed");
reject(new Error(`Provider Modify Error: ${err.message}`)); reject(new Error(`Provider Modify Error: ${err.message}`));
@ -110,7 +166,7 @@ export const ldapPasswordRotationFactory: TRotationFactory<
await $verifyCredentials({ dn, password }); await $verifyCredentials({ dn, password });
if (isPersonalRotation) { if (isConnectionRotation) {
const updatedCredentials: TLdapConnection["credentials"] = { const updatedCredentials: TLdapConnection["credentials"] = {
...credentials, ...credentials,
password password
@ -128,29 +184,41 @@ export const ldapPasswordRotationFactory: TRotationFactory<
return { dn, password }; return { dn, password };
}; };
const issueCredentials: TRotationFactoryIssueCredentials<TLdapPasswordRotationGeneratedCredentials> = async ( const issueCredentials: TRotationFactoryIssueCredentials<
callback TLdapPasswordRotationGeneratedCredentials,
) => { TLdapPasswordRotationInput["temporaryParameters"]
const credentials = await $rotatePassword(); > = async (callback, temporaryParameters) => {
const credentials = await $rotatePassword(
parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal
? temporaryParameters?.password
: undefined
);
return callback(credentials); return callback(credentials);
}; };
const revokeCredentials: TRotationFactoryRevokeCredentials<TLdapPasswordRotationGeneratedCredentials> = async ( const revokeCredentials: TRotationFactoryRevokeCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
_, credentialsToRevoke,
callback callback
) => { ) => {
const currentPassword = credentialsToRevoke[activeIndex].password;
// we just rotate to a new password, essentially revoking old credentials // we just rotate to a new password, essentially revoking old credentials
await $rotatePassword(); await $rotatePassword(
parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal ? currentPassword : undefined
);
return callback(); return callback();
}; };
const rotateCredentials: TRotationFactoryRotateCredentials<TLdapPasswordRotationGeneratedCredentials> = async ( const rotateCredentials: TRotationFactoryRotateCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
_, _,
callback callback,
activeCredentials
) => { ) => {
const credentials = await $rotatePassword(); const credentials = await $rotatePassword(
parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal ? activeCredentials.password : undefined
);
return callback(credentials); return callback(credentials);
}; };

View File

@ -1,6 +1,7 @@
import RE2 from "re2"; import RE2 from "re2";
import { z } from "zod"; 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 { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { import {
BaseCreateSecretRotationSchema, BaseCreateSecretRotationSchema,
@ -9,7 +10,7 @@ import {
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general"; import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general";
import { SecretRotations } from "@app/lib/api-docs"; 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 { SecretNameSchema } from "@app/server/lib/schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
@ -26,10 +27,16 @@ const LdapPasswordRotationParametersSchema = z.object({
dn: z dn: z
.string() .string()
.trim() .trim()
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com") .min(1, "DN/UPN required")
.min(1, "Distinguished Name (DN) Required") .refine((value) => new RE2(DistinguishedNameRegex).test(value) || new RE2(UserPrincipalNameRegex).test(value), {
message: "Invalid DN/UPN format"
})
.describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn), .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({ const LdapPasswordRotationSecretsMappingSchema = z.object({
@ -50,10 +57,28 @@ export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotatio
secretsMapping: LdapPasswordRotationSecretsMappingSchema secretsMapping: LdapPasswordRotationSecretsMappingSchema
}); });
export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({ export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword)
parameters: LdapPasswordRotationParametersSchema, .extend({
secretsMapping: LdapPasswordRotationSecretsMappingSchema 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({ export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({
parameters: LdapPasswordRotationParametersSchema.optional(), parameters: LdapPasswordRotationParametersSchema.optional(),

View File

@ -9,6 +9,11 @@ import {
LdapPasswordRotationSchema LdapPasswordRotationSchema
} from "./ldap-password-rotation-schemas"; } from "./ldap-password-rotation-schemas";
export enum LdapPasswordRotationMethod {
ConnectionPrincipal = "connection-principal",
TargetPrincipal = "target-principal"
}
export type TLdapPasswordRotation = z.infer<typeof LdapPasswordRotationSchema>; export type TLdapPasswordRotation = z.infer<typeof LdapPasswordRotationSchema>;
export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>; export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>;

View File

@ -1,12 +1,13 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret"; 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 { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-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 { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials"; import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums"; import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
@ -15,7 +16,8 @@ import {
TSecretRotationV2, TSecretRotationV2,
TSecretRotationV2GeneratedCredentials, TSecretRotationV2GeneratedCredentials,
TSecretRotationV2ListItem, TSecretRotationV2ListItem,
TSecretRotationV2Raw TSecretRotationV2Raw,
TUpdateSecretRotationV2DTO
} from "./secret-rotation-v2-types"; } from "./secret-rotation-v2-types";
const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = { const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2ListItem> = {
@ -228,3 +230,30 @@ export const parseRotationErrorMessage = (err: unknown): string => {
? errorMessage ? errorMessage
: `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`; : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
}; };
function haveUnequalProperties<T>(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
}
};

View File

@ -25,7 +25,8 @@ import {
getNextUtcRotationInterval, getNextUtcRotationInterval,
getSecretRotationRotateSecretJobOptions, getSecretRotationRotateSecretJobOptions,
listSecretRotationOptions, listSecretRotationOptions,
parseRotationErrorMessage parseRotationErrorMessage,
throwOnImmutableParameterUpdate
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-fns";
import { import {
SECRET_ROTATION_CONNECTION_MAP, SECRET_ROTATION_CONNECTION_MAP,
@ -46,6 +47,7 @@ import {
TSecretRotationV2, TSecretRotationV2,
TSecretRotationV2GeneratedCredentials, TSecretRotationV2GeneratedCredentials,
TSecretRotationV2Raw, TSecretRotationV2Raw,
TSecretRotationV2TemporaryParameters,
TSecretRotationV2WithConnection, TSecretRotationV2WithConnection,
TUpdateSecretRotationV2DTO TUpdateSecretRotationV2DTO
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
@ -112,7 +114,8 @@ const MAX_GENERATED_CREDENTIALS_LENGTH = 2;
type TRotationFactoryImplementation = TRotationFactory< type TRotationFactoryImplementation = TRotationFactory<
TSecretRotationV2WithConnection, TSecretRotationV2WithConnection,
TSecretRotationV2GeneratedCredentials TSecretRotationV2GeneratedCredentials,
TSecretRotationV2TemporaryParameters
>; >;
const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = { const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplementation> = {
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation, [SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
@ -400,6 +403,7 @@ export const secretRotationV2ServiceFactory = ({
environment, environment,
rotateAtUtc = { hours: 0, minutes: 0 }, rotateAtUtc = { hours: 0, minutes: 0 },
secretsMapping, secretsMapping,
temporaryParameters,
...payload ...payload
}: TCreateSecretRotationV2DTO, }: TCreateSecretRotationV2DTO,
actor: OrgServiceActor actor: OrgServiceActor
@ -546,7 +550,7 @@ export const secretRotationV2ServiceFactory = ({
return createdRotation; return createdRotation;
}); });
}); }, temporaryParameters);
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId); await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
await snapshotService.performSnapshot(folder.id); await snapshotService.performSnapshot(folder.id);
@ -585,10 +589,7 @@ export const secretRotationV2ServiceFactory = ({
} }
}; };
const updateSecretRotation = async ( const updateSecretRotation = async (dto: TUpdateSecretRotationV2DTO, actor: OrgServiceActor) => {
{ type, rotationId, ...payload }: TUpdateSecretRotationV2DTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId); const plan = await licenseService.getPlan(actor.orgId);
if (!plan.secretRotation) 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." 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); const secretRotation = await secretRotationV2DAL.findById(rotationId);
if (!secretRotation) if (!secretRotation)
@ -603,6 +606,8 @@ export const secretRotationV2ServiceFactory = ({
message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}` message: `Could not find ${SECRET_ROTATION_NAME_MAP[type]} Rotation with ID ${rotationId}`
}); });
throwOnImmutableParameterUpdate(dto, secretRotation);
const { folder, environment, projectId, folderId, connection } = secretRotation; const { folder, environment, projectId, folderId, connection } = secretRotation;
const secretsMapping = secretRotation.secretsMapping as TSecretRotationV2["secretsMapping"]; const secretsMapping = secretRotation.secretsMapping as TSecretRotationV2["secretsMapping"];
@ -877,6 +882,7 @@ export const secretRotationV2ServiceFactory = ({
const inactiveIndex = (activeIndex + 1) % MAX_GENERATED_CREDENTIALS_LENGTH; const inactiveIndex = (activeIndex + 1) % MAX_GENERATED_CREDENTIALS_LENGTH;
const inactiveCredentials = generatedCredentials[inactiveIndex]; const inactiveCredentials = generatedCredentials[inactiveIndex];
const activeCredentials = generatedCredentials[activeIndex];
const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation]( const rotationFactory = SECRET_ROTATION_FACTORY_MAP[type as SecretRotation](
{ {
@ -887,73 +893,77 @@ export const secretRotationV2ServiceFactory = ({
kmsService kmsService
); );
const updatedRotation = await rotationFactory.rotateCredentials(inactiveCredentials, async (newCredentials) => { const updatedRotation = await rotationFactory.rotateCredentials(
const updatedCredentials = [...generatedCredentials]; inactiveCredentials,
updatedCredentials[inactiveIndex] = newCredentials; async (newCredentials) => {
const updatedCredentials = [...generatedCredentials];
updatedCredentials[inactiveIndex] = newCredentials;
const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({ const encryptedUpdatedCredentials = await encryptSecretRotationCredentials({
projectId, projectId,
generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials, generatedCredentials: updatedCredentials as TSecretRotationV2GeneratedCredentials,
kmsService kmsService
});
return secretRotationV2DAL.transaction(async (tx) => {
const secretsPayload = rotationFactory.getSecretsPayload(newCredentials);
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
}); });
// update mapped secrets with new credential values return secretRotationV2DAL.transaction(async (tx) => {
await fnSecretBulkUpdate({ const secretsPayload = rotationFactory.getSecretsPayload(newCredentials);
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(); const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
return secretRotationV2DAL.updateById( // update mapped secrets with new credential values
secretRotation.id, await fnSecretBulkUpdate({
{ folderId,
encryptedGeneratedCredentials: encryptedUpdatedCredentials, orgId: connection.orgId,
activeIndex: inactiveIndex, tx,
isLastRotationManual: isManualRotation, inputSecrets: secretsPayload.map(({ key, value }) => ({
lastRotatedAt: currentTime, filter: {
lastRotationAttemptedAt: currentTime, key,
nextRotationAt: calculateNextRotationAt({ folderId,
...(secretRotation as TSecretRotationV2), type: SecretType.Shared
rotationStatus: SecretRotationStatus.Success, },
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, lastRotatedAt: currentTime,
isManualRotation lastRotationAttemptedAt: currentTime,
}), nextRotationAt: calculateNextRotationAt({
rotationStatus: SecretRotationStatus.Success, ...(secretRotation as TSecretRotationV2),
lastRotationJobId: jobId, rotationStatus: SecretRotationStatus.Success,
encryptedLastRotationMessage: null lastRotatedAt: currentTime,
}, isManualRotation
tx }),
); rotationStatus: SecretRotationStatus.Success,
}); lastRotationJobId: jobId,
}); encryptedLastRotationMessage: null
},
tx
);
});
},
activeCredentials
);
await auditLogService.createAuditLog({ await auditLogService.createAuditLog({
...(auditLogInfo ?? { ...(auditLogInfo ?? {

View File

@ -87,6 +87,8 @@ export type TSecretRotationV2ListItem =
| TLdapPasswordRotationListItem | TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem; | TAwsIamUserSecretRotationListItem;
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>; export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
export type TListSecretRotationsV2ByProjectId = { export type TListSecretRotationsV2ByProjectId = {
@ -120,6 +122,7 @@ export type TCreateSecretRotationV2DTO = Pick<
environment: string; environment: string;
isAutoRotationEnabled?: boolean; isAutoRotationEnabled?: boolean;
rotateAtUtc?: TRotateAtUtc; rotateAtUtc?: TRotateAtUtc;
temporaryParameters?: TSecretRotationV2TemporaryParameters;
}; };
export type TUpdateSecretRotationV2DTO = Partial< 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 // 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 // third party credential changes (when supported), preventing credentials getting out of sync
export type TRotationFactoryIssueCredentials<T extends TSecretRotationV2GeneratedCredentials> = ( export type TRotationFactoryIssueCredentials<
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw> T extends TSecretRotationV2GeneratedCredentials,
P extends TSecretRotationV2TemporaryParameters = undefined
> = (
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>,
temporaryParameters?: P
) => Promise<TSecretRotationV2Raw>; ) => Promise<TSecretRotationV2Raw>;
export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2GeneratedCredentials> = ( export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
@ -197,7 +204,8 @@ export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2Generat
export type TRotationFactoryRotateCredentials<T extends TSecretRotationV2GeneratedCredentials> = ( export type TRotationFactoryRotateCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
credentialsToRevoke: T[number] | undefined, credentialsToRevoke: T[number] | undefined,
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw> callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>,
activeCredentials: T[number]
) => Promise<TSecretRotationV2Raw>; ) => Promise<TSecretRotationV2Raw>;
export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2GeneratedCredentials> = ( export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2GeneratedCredentials> = (
@ -206,13 +214,14 @@ export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2Generat
export type TRotationFactory< export type TRotationFactory<
T extends TSecretRotationV2WithConnection, T extends TSecretRotationV2WithConnection,
C extends TSecretRotationV2GeneratedCredentials C extends TSecretRotationV2GeneratedCredentials,
P extends TSecretRotationV2TemporaryParameters = undefined
> = ( > = (
secretRotation: T, secretRotation: T,
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">, appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey"> kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
) => { ) => {
issueCredentials: TRotationFactoryIssueCredentials<C>; issueCredentials: TRotationFactoryIssueCredentials<C, P>;
revokeCredentials: TRotationFactoryRevokeCredentials<C>; revokeCredentials: TRotationFactoryRevokeCredentials<C>;
rotateCredentials: TRotationFactoryRotateCredentials<C>; rotateCredentials: TRotationFactoryRotateCredentials<C>;
getSecretsPayload: TRotationFactoryGetSecretsPayload<C>; getSecretsPayload: TRotationFactoryGetSecretsPayload<C>;

View File

@ -2060,7 +2060,7 @@ export const AppConnections = {
LDAP: { LDAP: {
provider: "The type of LDAP provider. Determines provider-specific behaviors.", 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').", 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.", password: "The password to bind with for authentication.",
sslRejectUnauthorized: sslRejectUnauthorized:
"Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.", "Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.",
@ -2305,7 +2305,10 @@ export const SecretRotations = {
clientId: "The client ID of the Azure Application to rotate the client secret for." clientId: "The client ID of the Azure Application to rotate the client secret for."
}, },
LDAP_PASSWORD: { 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: { GENERAL: {
PASSWORD_REQUIREMENTS: { PASSWORD_REQUIREMENTS: {
@ -2339,7 +2342,7 @@ export const SecretRotations = {
clientSecret: "The name of the secret that the rotated client secret will be mapped to." clientSecret: "The name of the secret that the rotated client secret will be mapped to."
}, },
LDAP_PASSWORD: { 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." password: "The name of the secret that the rotated password will be mapped to."
}, },
AWS_IAM_USER_SECRET: { AWS_IAM_USER_SECRET: {

View File

@ -1,3 +1,5 @@
export const DistinguishedNameRegex = export const DistinguishedNameRegex =
// DN format, ie; CN=user,OU=users,DC=example,DC=com // 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]+=[^,+="<>#;\\\\]+)*))*)?$/; /^(?:(?:[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,}$/;

View File

@ -2,7 +2,7 @@ import RE2 from "re2";
import { z } from "zod"; import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs"; import { AppConnections } from "@app/lib/api-docs";
import { DistinguishedNameRegex } from "@app/lib/regex"; import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/lib/regex";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { import {
BaseAppConnectionSchema, BaseAppConnectionSchema,
@ -23,8 +23,10 @@ export const LdapConnectionSimpleBindCredentialsSchema = z.object({
dn: z dn: z
.string() .string()
.trim() .trim()
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com") .min(1, "DN/UPN required")
.min(1, "Distinguished Name (DN) required") .refine((value) => new RE2(DistinguishedNameRegex).test(value) || new RE2(UserPrincipalNameRegex).test(value), {
message: "Invalid DN/UPN format"
})
.describe(AppConnections.CREDENTIALS.LDAP.dn), .describe(AppConnections.CREDENTIALS.LDAP.dn),
password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password), password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password),
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized), sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized),

View File

@ -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**. 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) ![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.
<Note> <Note>
LDAP Password Rotations require an LDAP Connection that uses ldaps:// protocol. LDAP Password Rotations require an LDAP Connection that uses ldaps:// protocol.
</Note> </Note>
@ -40,13 +40,20 @@ description: "Learn how to automatically rotate LDAP passwords."
</Note> </Note>
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 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) 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**. 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) ![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. - **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**. 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 "minutes": 0
}, },
"parameters": { "parameters": {
"rotationMethod": "connection-principal",
"dn": "CN=John,CN=Users,DC=example,DC=com", "dn": "CN=John,CN=Users,DC=example,DC=com",
"passwordRequirements": { "passwordRequirements": {
"length": 48, "length": 48,
@ -154,6 +162,7 @@ description: "Learn how to automatically rotate LDAP passwords."
"lastRotationMessage": null, "lastRotationMessage": null,
"type": "ldap-password", "type": "ldap-password",
"parameters": { "parameters": {
"rotationMethod": "connection-principal",
"dn": "CN=John,CN=Users,DC=example,DC=com", "dn": "CN=John,CN=Users,DC=example,DC=com",
"passwordRequirements": { "passwordRequirements": {
"length": 48, "length": 48,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 KiB

After

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 782 KiB

After

Width:  |  Height:  |  Size: 791 KiB

View File

@ -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: 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) - **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) 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 - **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 - **CA Certificate** - The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate

View File

@ -18,9 +18,7 @@ export const ViewLdapPasswordRotationGeneratedCredentials = ({
<ViewRotationGeneratedCredentialsDisplay <ViewRotationGeneratedCredentialsDisplay
activeCredentials={ activeCredentials={
<> <>
<CredentialDisplay label="Distinguished Name (DN)"> <CredentialDisplay label="DN/UPN">{activeCredentials?.dn}</CredentialDisplay>
{activeCredentials?.dn}
</CredentialDisplay>
<CredentialDisplay isSensitive label="Password"> <CredentialDisplay isSensitive label="Password">
{activeCredentials?.password} {activeCredentials?.password}
</CredentialDisplay> </CredentialDisplay>
@ -28,9 +26,7 @@ export const ViewLdapPasswordRotationGeneratedCredentials = ({
} }
inactiveCredentials={ inactiveCredentials={
<> <>
<CredentialDisplay label="Distinguished Name (DN)"> <CredentialDisplay label="DN/UPN">{inactiveCredentials?.dn}</CredentialDisplay>
{inactiveCredentials?.dn}
</CredentialDisplay>
<CredentialDisplay isSensitive label="Password"> <CredentialDisplay isSensitive label="Password">
{inactiveCredentials?.password} {inactiveCredentials?.password}
</CredentialDisplay> </CredentialDisplay>

View File

@ -48,7 +48,8 @@ const FORM_TABS: { name: string; key: string; fields: (keyof TSecretRotationV2Fo
"rotateAtUtc" "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: "Mappings", key: "secretsMapping", fields: ["secretsMapping"] },
{ name: "Details", key: "details", fields: ["name", "description"] }, { name: "Details", key: "details", fields: ["name", "description"] },
{ name: "Review", key: "review", fields: [] } { name: "Review", key: "review", fields: [] }
@ -75,7 +76,7 @@ export const SecretRotationV2Form = ({
const { rotationOption } = useSecretRotationV2Option(type); const { rotationOption } = useSecretRotationV2Option(type);
const formMethods = useForm<TSecretRotationV2Form>({ const formMethods = useForm<TSecretRotationV2Form>({
resolver: zodResolver(SecretRotationV2FormSchema), resolver: zodResolver(SecretRotationV2FormSchema(Boolean(secretRotation))),
defaultValues: secretRotation defaultValues: secretRotation
? { ? {
...secretRotation, ...secretRotation,

View File

@ -2,40 +2,123 @@ import { Controller, useFormContext } from "react-hook-form";
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas"; import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
import { DEFAULT_PASSWORD_REQUIREMENTS } from "@app/components/secret-rotations-v2/forms/schemas/shared"; 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 { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation";
export const LdapPasswordRotationParametersFields = () => { export const LdapPasswordRotationParametersFields = () => {
const { control } = useFormContext< const { control, watch, setValue } = useFormContext<
TSecretRotationV2Form & { TSecretRotationV2Form & {
type: SecretRotation.LdapPassword; type: SecretRotation.LdapPassword;
} }
>(); >();
const [id, rotationMethod] = watch(["id", "parameters.rotationMethod"]);
const isUpdate = Boolean(id);
return ( return (
<> <>
<Controller <Controller
name="parameters.dn" name="parameters.rotationMethod"
control={control} control={control}
defaultValue={LdapPasswordRotationMethod.ConnectionPrincipal}
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
isError={Boolean(error)} tooltipText={
<>
<span>Determines how the rotation will be performed:</span>
<ul className="ml-4 mt-2 flex list-disc flex-col gap-2">
<li>
<span className="font-medium">Connection Principal</span> - The Connection
principal will rotate the target principal&#39;s password.
</li>
<li>
<span className="font-medium">Target Principal</span> - The target principal
will rotate their own password.
</li>
</ul>
</>
}
tooltipClassName="max-w-sm"
errorText={error?.message} errorText={error?.message}
label="Distinguished Name (DN)" isError={Boolean(error?.message)}
label="Rotation Method"
helperText={isUpdate ? "Cannot be updated." : undefined}
> >
<Input <Select
isDisabled={isUpdate}
value={value} value={value}
onChange={onChange} onValueChange={(val) => {
placeholder="CN=John,OU=Users,DC=example,DC=com" 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 (
<SelectItem value={method} className="capitalize" key={method}>
{method.replace("-", " ")}
</SelectItem>
);
})}
</Select>
</FormControl> </FormControl>
)} )}
/> />
<div className="flex gap-3">
<Controller
name="parameters.dn"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
className="flex-1"
isError={Boolean(error)}
errorText={error?.message}
label="Target Principal's DN/UPN"
tooltipText="The DN/UPN of the principal that you want to peform password rotation on."
tooltipClassName="max-w-sm"
helperText={isUpdate ? "Cannot be updated." : undefined}
>
<Input
isDisabled={isUpdate}
value={value}
onChange={onChange}
placeholder="CN=John,OU=Users,DC=example,DC=com"
/>
</FormControl>
)}
/>
{rotationMethod === LdapPasswordRotationMethod.TargetPrincipal && !isUpdate && (
<Controller
name="temporaryParameters.password"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
className="flex-1"
isError={Boolean(error)}
errorText={error?.message}
label="Target Principal's Password"
>
<Input value={value} onChange={onChange} placeholder="***********************" />
</FormControl>
)}
/>
)}
</div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="w-full border-b border-mineshaft-600"> <div className="w-full border-b border-mineshaft-600">
<span className="text-sm text-mineshaft-300">Password Requirements</span> <span className="text-sm text-mineshaft-300">Password Requirements</span>
</div> </div>
<div className="grid grid-cols-2 gap-3 rounded border border-mineshaft-600 bg-mineshaft-700 px-3 py-2"> <div className="grid grid-cols-2 gap-x-3 gap-y-1 rounded border border-mineshaft-600 bg-mineshaft-700 px-3 pt-2">
<Controller <Controller
control={control} control={control}
name="parameters.passwordRequirements.length" name="parameters.passwordRequirements.length"

View File

@ -15,13 +15,35 @@ export const LdapPasswordRotationReviewFields = () => {
const [parameters, { dn, password }] = watch(["parameters", "secretsMapping"]); const [parameters, { dn, password }] = watch(["parameters", "secretsMapping"]);
const { passwordRequirements } = parameters;
return ( return (
<> <>
<SecretRotationReviewSection label="Parameters"> <SecretRotationReviewSection label="Parameters">
<GenericFieldLabel label="Distinguished Name (DN)">{parameters.dn}</GenericFieldLabel> <GenericFieldLabel label="DN/UPN">{parameters.dn}</GenericFieldLabel>
</SecretRotationReviewSection> </SecretRotationReviewSection>
{passwordRequirements && (
<SecretRotationReviewSection label="Password Requirements">
<GenericFieldLabel label="Length">{passwordRequirements.length}</GenericFieldLabel>
<GenericFieldLabel label="Minimum Digits">
{passwordRequirements.required.digits}
</GenericFieldLabel>
<GenericFieldLabel label="Minimum Lowercase Characters">
{passwordRequirements.required.lowercase}
</GenericFieldLabel>
<GenericFieldLabel label="Minimum Uppercase Characters">
{passwordRequirements.required.uppercase}
</GenericFieldLabel>
<GenericFieldLabel label="Minimum Symbols">
{passwordRequirements.required.symbols}
</GenericFieldLabel>
<GenericFieldLabel label="Allowed Symbols">
{passwordRequirements.allowedSymbols}
</GenericFieldLabel>
</SecretRotationReviewSection>
)}
<SecretRotationReviewSection label="Secrets Mapping"> <SecretRotationReviewSection label="Secrets Mapping">
<GenericFieldLabel label="Distinguished Name (DN)">{dn}</GenericFieldLabel> <GenericFieldLabel label="DN/UPN">{dn}</GenericFieldLabel>
<GenericFieldLabel label="Password">{password}</GenericFieldLabel> <GenericFieldLabel label="Password">{password}</GenericFieldLabel>
</SecretRotationReviewSection> </SecretRotationReviewSection>
</> </>

View File

@ -1,7 +1,7 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
type Props = { type Props = {
label: "Parameters" | "Secrets Mapping"; label: "Parameters" | "Secrets Mapping" | "Password Requirements";
children: ReactNode; children: ReactNode;
}; };

View File

@ -17,7 +17,7 @@ export const LdapPasswordRotationSecretsMappingFields = () => {
const items = [ const items = [
{ {
name: "DN", name: "DN/UPN",
input: ( input: (
<Controller <Controller
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (

View File

@ -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 { 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 { 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 { 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", [ export const SecretRotationV2FormSchema = (isUpdate: boolean) =>
Auth0ClientSecretRotationSchema, z
AzureClientSecretRotationSchema, .intersection(
PostgresCredentialsRotationSchema, z.discriminatedUnion("type", [
MsSqlCredentialsRotationSchema, Auth0ClientSecretRotationSchema,
LdapPasswordRotationSchema, AzureClientSecretRotationSchema,
AwsIamUserSecretRotationSchema 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<typeof SecretRotationV2FormSchema>; export type TSecretRotationV2Form = z.infer<ReturnType<typeof SecretRotationV2FormSchema>>;

View File

@ -2,8 +2,9 @@ import { z } from "zod";
import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema"; 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 { 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 { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { LdapPasswordRotationMethod } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation";
export const LdapPasswordRotationSchema = z export const LdapPasswordRotationSchema = z
.object({ .object({
@ -12,13 +13,24 @@ export const LdapPasswordRotationSchema = z
dn: z dn: z
.string() .string()
.trim() .trim()
.regex(DistinguishedNameRegex, "Invalid Distinguished Name format") .min(1, "DN/UPN required")
.min(1, "Distinguished Name (DN) required"), .refine(
passwordRequirements: PasswordRequirementsSchema.optional() (value) => DistinguishedNameRegex.test(value) || UserPrincipalNameRegex.test(value),
{
message: "Invalid DN/UPN format"
}
),
passwordRequirements: PasswordRequirementsSchema.optional(),
rotationMethod: z.nativeEnum(LdapPasswordRotationMethod).optional()
}), }),
secretsMapping: z.object({ 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") password: z.string().trim().min(1, "Password required")
}) }),
temporaryParameters: z
.object({
password: z.string().min(1, "Password required")
})
.optional()
}) })
.merge(BaseSecretRotationSchema); .merge(BaseSecretRotationSchema);

View File

@ -1,5 +1,7 @@
import { z } from "zod"; import { z } from "zod";
export type TPasswordRequirements = z.infer<typeof PasswordRequirementsSchema>;
export const PasswordRequirementsSchema = z export const PasswordRequirementsSchema = z
.object({ .object({
length: z length: z

View File

@ -144,6 +144,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
<a <a
href="https://infisical.com/docs/integrations/secret-syncs/overview#key-schemas" href="https://infisical.com/docs/integrations/secret-syncs/overview#key-schemas"
target="_blank" target="_blank"
rel="noreferrer"
> >
Key Schema Key Schema
</a>{" "} </a>{" "}

View File

@ -15,3 +15,5 @@ export const isValidPath = (val: string): boolean => {
export const DistinguishedNameRegex = export const DistinguishedNameRegex =
/^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/; /^(?:(?:[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,}$/;

View File

@ -1,3 +1,4 @@
import { TPasswordRequirements } from "@app/components/secret-rotations-v2/forms/schemas/shared";
import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { import {
@ -5,10 +6,17 @@ import {
TSecretRotationV2GeneratedCredentialsResponseBase TSecretRotationV2GeneratedCredentialsResponseBase
} from "@app/hooks/api/secretRotationsV2/types/shared"; } from "@app/hooks/api/secretRotationsV2/types/shared";
export enum LdapPasswordRotationMethod {
ConnectionPrincipal = "connection-principal",
TargetPrincipal = "target-principal"
}
export type TLdapPasswordRotation = TSecretRotationV2Base & { export type TLdapPasswordRotation = TSecretRotationV2Base & {
type: SecretRotation.LdapPassword; type: SecretRotation.LdapPassword;
parameters: { parameters: {
dn: string; dn: string;
method?: LdapPasswordRotationMethod;
passwordRequirements?: TPasswordRequirements;
}; };
secretsMapping: { secretsMapping: {
dn: string; dn: string;

View File

@ -19,7 +19,7 @@ import {
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { DistinguishedNameRegex } from "@app/helpers/string"; import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/helpers/string";
import { import {
LdapConnectionMethod, LdapConnectionMethod,
LdapConnectionProvider, LdapConnectionProvider,
@ -55,8 +55,13 @@ const formSchema = z.discriminatedUnion("method", [
dn: z dn: z
.string() .string()
.trim() .trim()
.regex(DistinguishedNameRegex, "Invalid Distinguished Name format") .min(1, "DN/UPN required")
.min(1, "Distinguished Name (DN) required"), .refine(
(value) => DistinguishedNameRegex.test(value) || UserPrincipalNameRegex.test(value),
{
message: "Invalid DN/UPN format"
}
),
password: z.string().trim().min(1, "Password required"), password: z.string().trim().min(1, "Password required"),
sslRejectUnauthorized: z.boolean(), sslRejectUnauthorized: z.boolean(),
sslCertificate: z sslCertificate: z
@ -223,7 +228,7 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {
<FormControl <FormControl
errorText={error?.message} errorText={error?.message}
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
label="Binding Distinguished Name (DN)" label="Binding DN/UPN"
> >
<Input {...field} placeholder="CN=John,OU=Users,DC=example,DC=com" /> <Input {...field} placeholder="CN=John,OU=Users,DC=example,DC=com" />
</FormControl> </FormControl>