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 {
|
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);
|
||||||
};
|
};
|
||||||
|
@ -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(),
|
||||||
|
@ -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>;
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -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 ?? {
|
||||||
|
@ -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>;
|
||||||
|
@ -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: {
|
||||||
|
@ -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,}$/;
|
||||||
|
@ -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),
|
||||||
|
@ -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**.
|
||||||

|

|
||||||
|
|
||||||
- **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 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**.
|
||||||

|

|
||||||
|
|
||||||
- **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 |
@ -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
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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'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"
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 } }) => (
|
||||||
|
@ -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>>;
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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>{" "}
|
||||||
|
@ -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,}$/;
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
Reference in New Issue
Block a user