mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-13 09:35:39 +00:00
Compare commits
1 Commits
commit-ui-
...
ldap-rotat
Author | SHA1 | Date | |
---|---|---|---|
83772080ef |
@ -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<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<
|
||||
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<TLdapConnection["credentials"], "dn" | "password">) => {
|
||||
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<TLdapPasswordRotationGeneratedCredentials> = 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<TLdapPasswordRotationGeneratedCredentials> = 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<TLdapPasswordRotationGeneratedCredentials> = async (
|
||||
_,
|
||||
callback
|
||||
callback,
|
||||
activeCredentials
|
||||
) => {
|
||||
const credentials = await $rotatePassword();
|
||||
const credentials = await $rotatePassword(
|
||||
parameters.rotationMethod === LdapPasswordRotationMethod.TargetPrincipal ? activeCredentials.password : undefined
|
||||
);
|
||||
|
||||
return callback(credentials);
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
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 +10,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 +27,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) => new RE2(DistinguishedNameRegex).test(value) || new RE2(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 +57,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(),
|
||||
|
@ -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<typeof LdapPasswordRotationSchema>;
|
||||
|
||||
export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>;
|
||||
|
@ -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<SecretRotation, TSecretRotationV2ListItem> = {
|
||||
@ -228,3 +230,30 @@ export const parseRotationErrorMessage = (err: unknown): string => {
|
||||
? errorMessage
|
||||
: `${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
|
||||
}
|
||||
};
|
||||
|
@ -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, TRotationFactoryImplementation> = {
|
||||
[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 ?? {
|
||||
|
@ -87,6 +87,8 @@ export type TSecretRotationV2ListItem =
|
||||
| TLdapPasswordRotationListItem
|
||||
| TAwsIamUserSecretRotationListItem;
|
||||
|
||||
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;
|
||||
|
||||
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;
|
||||
|
||||
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<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
|
||||
export type TRotationFactoryIssueCredentials<
|
||||
T extends TSecretRotationV2GeneratedCredentials,
|
||||
P extends TSecretRotationV2TemporaryParameters = undefined
|
||||
> = (
|
||||
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>,
|
||||
temporaryParameters?: P
|
||||
) => Promise<TSecretRotationV2Raw>;
|
||||
|
||||
export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
@ -197,7 +204,8 @@ export type TRotationFactoryRevokeCredentials<T extends TSecretRotationV2Generat
|
||||
|
||||
export type TRotationFactoryRotateCredentials<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
credentialsToRevoke: T[number] | undefined,
|
||||
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>
|
||||
callback: (newCredentials: T[number]) => Promise<TSecretRotationV2Raw>,
|
||||
activeCredentials: T[number]
|
||||
) => Promise<TSecretRotationV2Raw>;
|
||||
|
||||
export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2GeneratedCredentials> = (
|
||||
@ -206,13 +214,14 @@ export type TRotationFactoryGetSecretsPayload<T extends TSecretRotationV2Generat
|
||||
|
||||
export type TRotationFactory<
|
||||
T extends TSecretRotationV2WithConnection,
|
||||
C extends TSecretRotationV2GeneratedCredentials
|
||||
C extends TSecretRotationV2GeneratedCredentials,
|
||||
P extends TSecretRotationV2TemporaryParameters = undefined
|
||||
> = (
|
||||
secretRotation: T,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
issueCredentials: TRotationFactoryIssueCredentials<C>;
|
||||
issueCredentials: TRotationFactoryIssueCredentials<C, P>;
|
||||
revokeCredentials: TRotationFactoryRevokeCredentials<C>;
|
||||
rotateCredentials: TRotationFactoryRotateCredentials<C>;
|
||||
getSecretsPayload: TRotationFactoryGetSecretsPayload<C>;
|
||||
|
@ -2060,7 +2060,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.",
|
||||
@ -2305,7 +2305,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: {
|
||||
@ -2339,7 +2342,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: {
|
||||
|
@ -1,3 +1,5 @@
|
||||
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]+=[^,+="<>#;\\\\]+)*))*)?$/;
|
||||
|
||||
export const UserPrincipalNameRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
|
@ -2,7 +2,7 @@ import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
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 {
|
||||
BaseAppConnectionSchema,
|
||||
@ -23,8 +23,10 @@ export const LdapConnectionSimpleBindCredentialsSchema = 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) => new RE2(DistinguishedNameRegex).test(value) || new RE2(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),
|
||||
|
@ -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**.
|
||||

|
||||
|
||||
- **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>
|
||||
LDAP Password Rotations require an LDAP Connection that uses ldaps:// protocol.
|
||||
</Note>
|
||||
@ -40,13 +40,20 @@ description: "Learn how to automatically rotate LDAP passwords."
|
||||
</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 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**.
|
||||

|
||||
|
||||
- **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,
|
||||
|
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 |
@ -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) 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
|
||||
|
||||
|
@ -18,9 +18,7 @@ export const ViewLdapPasswordRotationGeneratedCredentials = ({
|
||||
<ViewRotationGeneratedCredentialsDisplay
|
||||
activeCredentials={
|
||||
<>
|
||||
<CredentialDisplay label="Distinguished Name (DN)">
|
||||
{activeCredentials?.dn}
|
||||
</CredentialDisplay>
|
||||
<CredentialDisplay label="DN/UPN">{activeCredentials?.dn}</CredentialDisplay>
|
||||
<CredentialDisplay isSensitive label="Password">
|
||||
{activeCredentials?.password}
|
||||
</CredentialDisplay>
|
||||
@ -28,9 +26,7 @@ export const ViewLdapPasswordRotationGeneratedCredentials = ({
|
||||
}
|
||||
inactiveCredentials={
|
||||
<>
|
||||
<CredentialDisplay label="Distinguished Name (DN)">
|
||||
{inactiveCredentials?.dn}
|
||||
</CredentialDisplay>
|
||||
<CredentialDisplay label="DN/UPN">{inactiveCredentials?.dn}</CredentialDisplay>
|
||||
<CredentialDisplay isSensitive label="Password">
|
||||
{inactiveCredentials?.password}
|
||||
</CredentialDisplay>
|
||||
|
@ -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<TSecretRotationV2Form>({
|
||||
resolver: zodResolver(SecretRotationV2FormSchema),
|
||||
resolver: zodResolver(SecretRotationV2FormSchema(Boolean(secretRotation))),
|
||||
defaultValues: secretRotation
|
||||
? {
|
||||
...secretRotation,
|
||||
|
@ -2,40 +2,123 @@ 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 (
|
||||
<>
|
||||
<Controller
|
||||
name="parameters.dn"
|
||||
name="parameters.rotationMethod"
|
||||
control={control}
|
||||
defaultValue={LdapPasswordRotationMethod.ConnectionPrincipal}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<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'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}
|
||||
label="Distinguished Name (DN)"
|
||||
isError={Boolean(error?.message)}
|
||||
label="Rotation Method"
|
||||
helperText={isUpdate ? "Cannot be updated." : undefined}
|
||||
>
|
||||
<Input
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="CN=John,OU=Users,DC=example,DC=com"
|
||||
/>
|
||||
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 (
|
||||
<SelectItem value={method} className="capitalize" key={method}>
|
||||
{method.replace("-", " ")}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</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="w-full border-b border-mineshaft-600">
|
||||
<span className="text-sm text-mineshaft-300">Password Requirements</span>
|
||||
</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
|
||||
control={control}
|
||||
name="parameters.passwordRequirements.length"
|
||||
|
@ -15,13 +15,35 @@ export const LdapPasswordRotationReviewFields = () => {
|
||||
|
||||
const [parameters, { dn, password }] = watch(["parameters", "secretsMapping"]);
|
||||
|
||||
const { passwordRequirements } = parameters;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SecretRotationReviewSection label="Parameters">
|
||||
<GenericFieldLabel label="Distinguished Name (DN)">{parameters.dn}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="DN/UPN">{parameters.dn}</GenericFieldLabel>
|
||||
</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">
|
||||
<GenericFieldLabel label="Distinguished Name (DN)">{dn}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="DN/UPN">{dn}</GenericFieldLabel>
|
||||
<GenericFieldLabel label="Password">{password}</GenericFieldLabel>
|
||||
</SecretRotationReviewSection>
|
||||
</>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
label: "Parameters" | "Secrets Mapping";
|
||||
label: "Parameters" | "Secrets Mapping" | "Password Requirements";
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
|
@ -17,7 +17,7 @@ export const LdapPasswordRotationSecretsMappingFields = () => {
|
||||
|
||||
const items = [
|
||||
{
|
||||
name: "DN",
|
||||
name: "DN/UPN",
|
||||
input: (
|
||||
<Controller
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
|
@ -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<typeof SecretRotationV2FormSchema>;
|
||||
export type TSecretRotationV2Form = z.infer<ReturnType<typeof SecretRotationV2FormSchema>>;
|
||||
|
@ -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);
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export type TPasswordRequirements = z.infer<typeof PasswordRequirementsSchema>;
|
||||
|
||||
export const PasswordRequirementsSchema = z
|
||||
.object({
|
||||
length: z
|
||||
|
@ -144,6 +144,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/secret-syncs/overview#key-schemas"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Key Schema
|
||||
</a>{" "}
|
||||
|
@ -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,}$/;
|
||||
|
@ -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;
|
||||
method?: LdapPasswordRotationMethod;
|
||||
passwordRequirements?: TPasswordRequirements;
|
||||
};
|
||||
secretsMapping: {
|
||||
dn: string;
|
||||
|
@ -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) => {
|
||||
<FormControl
|
||||
errorText={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" />
|
||||
</FormControl>
|
||||
|
Reference in New Issue
Block a user