Compare commits

...

11 Commits

Author SHA1 Message Date
Scott Wilson
632572f7c3 Merge pull request #3452 from Infisical/ldaps-connection-and-password-rotation
Feature: LDAP Connection and Password Rotation
2025-04-26 09:13:08 -07:00
Scott Wilson
a524690d01 deconflict merge 2025-04-25 17:20:30 -07:00
Scott Wilson
f93edbb37f Merge pull request #3493 from Infisical/improve-aws-connection-error-propagation
improvement(app-connections): Improve AWS Connection Error Propagation
2025-04-25 15:25:55 -07:00
Scott Wilson
b329b5aa4b improvements: address feedback 2025-04-24 19:35:56 -07:00
Scott Wilson
e0dc2dd6d8 improvements: address feedback 2025-04-24 13:44:43 -07:00
Scott Wilson
33dea34061 chore: removed unused pick 2025-04-22 18:51:40 -07:00
Scott Wilson
da68073e86 chore: revert secret rotation flag 2025-04-22 18:06:44 -07:00
Scott Wilson
7bd312a287 improvements: update regex checks 2025-04-22 17:57:59 -07:00
Scott Wilson
d61e6752d6 Merge branch 'main' into ldaps-connection-and-password-rotation 2025-04-22 17:42:48 -07:00
Scott Wilson
636aee2ea9 improvements: address feedback 2025-04-22 17:36:18 -07:00
Scott Wilson
9032bbe514 feature: ldap connection and password rotation 2025-04-18 17:55:03 -07:00
97 changed files with 1965 additions and 58 deletions

View File

@@ -2,6 +2,7 @@ import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotat
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
@@ -14,5 +15,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
};

View File

@@ -0,0 +1,19 @@
import {
CreateLdapPasswordRotationSchema,
LdapPasswordRotationGeneratedCredentialsSchema,
LdapPasswordRotationSchema,
UpdateLdapPasswordRotationSchema
} from "@app/ee/services/secret-rotation-v2/ldap-password";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerLdapPasswordRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.LdapPassword,
server,
responseSchema: LdapPasswordRotationSchema,
createSchema: CreateLdapPasswordRotationSchema,
updateSchema: UpdateLdapPasswordRotationSchema,
generatedCredentialsSchema: LdapPasswordRotationGeneratedCredentialsSchema
});

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@@ -15,6 +16,7 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationListItemSchema,
MsSqlCredentialsRotationListItemSchema,
Auth0ClientSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema
]);

View File

@@ -0,0 +1,3 @@
export * from "./ldap-password-rotation-constants";
export * from "./ldap-password-rotation-schemas";
export * from "./ldap-password-rotation-types";

View File

@@ -0,0 +1,15 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const LDAP_PASSWORD_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "LDAP Password",
type: SecretRotation.LdapPassword,
connection: AppConnection.LDAP,
template: {
secretsMapping: {
dn: "LDAP_DN",
password: "LDAP_PASSWORD"
}
}
};

View File

@@ -0,0 +1,181 @@
import ldap from "ldapjs";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { logger } from "@app/lib/logger";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
import { generatePassword } from "../shared/utils";
import {
TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationWithConnection
} from "./ldap-password-rotation-types";
const getEncodedPassword = (password: string) => Buffer.from(`"${password}"`, "utf16le");
export const ldapPasswordRotationFactory: TRotationFactory<
TLdapPasswordRotationWithConnection,
TLdapPasswordRotationGeneratedCredentials
> = (secretRotation, appConnectionDAL, kmsService) => {
const {
connection,
parameters: { dn, passwordRequirements },
secretsMapping
} = secretRotation;
const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => {
try {
const client = await getLdapConnectionClient({ ...connection.credentials, ...credentials });
client.unbind();
client.destroy();
} catch (error) {
throw new Error(`Failed to verify credentials - ${(error as Error).message}`);
}
};
const $rotatePassword = async () => {
const { credentials, orgId } = connection;
if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection");
const client = await getLdapConnectionClient(credentials);
const isPersonalRotation = credentials.dn === dn;
const password = generatePassword(passwordRequirements);
let changes: ldap.Change[] | ldap.Change;
switch (credentials.provider) {
case LdapProvider.ActiveDirectory:
{
const encodedPassword = getEncodedPassword(password);
// service account vs personal password rotation require different changes
if (isPersonalRotation) {
const currentEncodedPassword = getEncodedPassword(credentials.password);
changes = [
new ldap.Change({
operation: "delete",
modification: {
type: "unicodePwd",
values: [currentEncodedPassword]
}
}),
new ldap.Change({
operation: "add",
modification: {
type: "unicodePwd",
values: [encodedPassword]
}
})
];
} else {
changes = new ldap.Change({
operation: "replace",
modification: {
type: "unicodePwd",
values: [encodedPassword]
}
});
}
}
break;
default:
throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`);
}
try {
await new Promise((resolve, reject) => {
client.modify(dn, changes, (err) => {
if (err) {
logger.error(err, "LDAP Password Rotation Failed");
reject(new Error(`Provider Modify Error: ${err.message}`));
} else {
resolve(true);
}
});
});
} finally {
client.unbind();
client.destroy();
}
await $verifyCredentials({ dn, password });
if (isPersonalRotation) {
const updatedCredentials: TLdapConnection["credentials"] = {
...credentials,
password
};
const encryptedCredentials = await encryptAppConnectionCredentials({
credentials: updatedCredentials,
orgId,
kmsService
});
await appConnectionDAL.updateById(connection.id, { encryptedCredentials });
}
return { dn, password };
};
const issueCredentials: TRotationFactoryIssueCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotatePassword();
return callback(credentials);
};
const revokeCredentials: TRotationFactoryRevokeCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
_,
callback
) => {
// we just rotate to a new password, essentially revoking old credentials
await $rotatePassword();
return callback();
};
const rotateCredentials: TRotationFactoryRotateCredentials<TLdapPasswordRotationGeneratedCredentials> = async (
_,
callback
) => {
const credentials = await $rotatePassword();
return callback(credentials);
};
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TLdapPasswordRotationGeneratedCredentials> = (
generatedCredentials
) => {
const secrets = [
{
key: secretsMapping.dn,
value: generatedCredentials.dn
},
{
key: secretsMapping.password,
value: generatedCredentials.password
}
];
return secrets;
};
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};

View File

@@ -0,0 +1,68 @@
import RE2 from "re2";
import { z } from "zod";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
BaseCreateSecretRotationSchema,
BaseSecretRotationSchema,
BaseUpdateSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
import { PasswordRequirementsSchema } from "@app/ee/services/secret-rotation-v2/shared/general";
import { SecretRotations } from "@app/lib/api-docs";
import { DistinguishedNameRegex } from "@app/lib/regex";
import { SecretNameSchema } from "@app/server/lib/schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const LdapPasswordRotationGeneratedCredentialsSchema = z
.object({
dn: z.string(),
password: z.string()
})
.array()
.min(1)
.max(2);
const LdapPasswordRotationParametersSchema = z.object({
dn: z
.string()
.trim()
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
.min(1, "Distinguished Name (DN) Required")
.describe(SecretRotations.PARAMETERS.LDAP_PASSWORD.dn),
passwordRequirements: PasswordRequirementsSchema.optional()
});
const LdapPasswordRotationSecretsMappingSchema = z.object({
dn: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.dn),
password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.LDAP_PASSWORD.password)
});
export const LdapPasswordRotationTemplateSchema = z.object({
secretsMapping: z.object({
dn: z.string(),
password: z.string()
})
});
export const LdapPasswordRotationSchema = BaseSecretRotationSchema(SecretRotation.LdapPassword).extend({
type: z.literal(SecretRotation.LdapPassword),
parameters: LdapPasswordRotationParametersSchema,
secretsMapping: LdapPasswordRotationSecretsMappingSchema
});
export const CreateLdapPasswordRotationSchema = BaseCreateSecretRotationSchema(SecretRotation.LdapPassword).extend({
parameters: LdapPasswordRotationParametersSchema,
secretsMapping: LdapPasswordRotationSecretsMappingSchema
});
export const UpdateLdapPasswordRotationSchema = BaseUpdateSecretRotationSchema(SecretRotation.LdapPassword).extend({
parameters: LdapPasswordRotationParametersSchema.optional(),
secretsMapping: LdapPasswordRotationSecretsMappingSchema.optional()
});
export const LdapPasswordRotationListItemSchema = z.object({
name: z.literal("LDAP Password"),
connection: z.literal(AppConnection.LDAP),
type: z.literal(SecretRotation.LdapPassword),
template: LdapPasswordRotationTemplateSchema
});

View File

@@ -0,0 +1,22 @@
import { z } from "zod";
import { TLdapConnection } from "@app/services/app-connection/ldap";
import {
CreateLdapPasswordRotationSchema,
LdapPasswordRotationGeneratedCredentialsSchema,
LdapPasswordRotationListItemSchema,
LdapPasswordRotationSchema
} from "./ldap-password-rotation-schemas";
export type TLdapPasswordRotation = z.infer<typeof LdapPasswordRotationSchema>;
export type TLdapPasswordRotationInput = z.infer<typeof CreateLdapPasswordRotationSchema>;
export type TLdapPasswordRotationListItem = z.infer<typeof LdapPasswordRotationListItemSchema>;
export type TLdapPasswordRotationWithConnection = TLdapPasswordRotation & {
connection: TLdapConnection;
};
export type TLdapPasswordRotationGeneratedCredentials = z.infer<typeof LdapPasswordRotationGeneratedCredentialsSchema>;

View File

@@ -2,6 +2,7 @@ export enum SecretRotation {
PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials",
Auth0ClientSecret = "auth0-client-secret",
LdapPassword = "ldap-password",
AwsIamUserSecret = "aws-iam-user-secret"
}

View File

@@ -5,6 +5,7 @@ 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 { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
@@ -20,6 +21,7 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
};

View File

@@ -5,6 +5,7 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
[SecretRotation.LdapPassword]: "LDAP Password",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret"
};
@@ -12,5 +13,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
};

View File

@@ -14,6 +14,7 @@ import {
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
import { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns";
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
calculateNextRotationAt,
@@ -116,6 +117,7 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
};
@@ -451,6 +453,18 @@ export const secretRotationV2ServiceFactory = ({
kmsService
);
// even though we have a db constraint we want to check before any rotation of credentials is attempted
// to prevent creation failure after external credentials have been modified
const conflictingRotation = await secretRotationV2DAL.findOne({
name: payload.name,
folderId: folder.id
});
if (conflictingRotation)
throw new BadRequestError({
message: `A Secret Rotation with the name "${payload.name}" already exists at the secret path "${secretPath}"`
});
try {
const currentTime = new Date();

View File

@@ -19,6 +19,13 @@ import {
TAwsIamUserSecretRotationListItem,
TAwsIamUserSecretRotationWithConnection
} from "./aws-iam-user-secret";
import {
TLdapPasswordRotation,
TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationInput,
TLdapPasswordRotationListItem,
TLdapPasswordRotationWithConnection
} from "./ldap-password";
import {
TMsSqlCredentialsRotation,
TMsSqlCredentialsRotationInput,
@@ -38,29 +45,34 @@ export type TSecretRotationV2 =
| TPostgresCredentialsRotation
| TMsSqlCredentialsRotation
| TAuth0ClientSecretRotation
| TLdapPasswordRotation
| TAwsIamUserSecretRotation;
export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection
| TMsSqlCredentialsRotationWithConnection
| TAuth0ClientSecretRotationWithConnection
| TLdapPasswordRotationWithConnection
| TAwsIamUserSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials
| TLdapPasswordRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials;
export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput
| TMsSqlCredentialsRotationInput
| TAuth0ClientSecretRotationInput
| TLdapPasswordRotationInput
| TAwsIamUserSecretRotationInput;
export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
| TMsSqlCredentialsRotationListItem
| TAuth0ClientSecretRotationListItem
| TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem;
export type TSecretRotationV2Raw = NonNullable<Awaited<ReturnType<TSecretRotationV2DALFactory["findById"]>>>;

View File

@@ -1,6 +1,7 @@
import { z } from "zod";
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
@@ -10,5 +11,6 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema,
Auth0ClientSecretRotationSchema,
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema
]);

View File

@@ -0,0 +1 @@
export * from "./password-requirements-schema";

View File

@@ -0,0 +1,44 @@
import RE2 from "re2";
import { z } from "zod";
import { SecretRotations } from "@app/lib/api-docs";
export const PasswordRequirementsSchema = z
.object({
length: z
.number()
.min(1, "Password length must be a positive number")
.max(250, "Password length must be less than 250")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.length),
required: z.object({
digits: z
.number()
.min(0, "Digit count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.digits),
lowercase: z
.number()
.min(0, "Lowercase count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.lowercase),
uppercase: z
.number()
.min(0, "Uppercase count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.uppercase),
symbols: z
.number()
.min(0, "Symbol count must be non-negative")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.required.symbols)
}),
allowedSymbols: z
.string()
.regex(new RE2("[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?~]"), "Invalid symbols")
.optional()
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.allowedSymbols)
})
.refine((data) => {
return Object.values(data.required).some((count) => count > 0);
}, "At least one character type must be required")
.refine((data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
return total <= data.length;
}, "Sum of required characters cannot exceed the total length")
.describe(SecretRotations.PARAMETERS.GENERAL.PASSWORD_REQUIREMENTS.base);

View File

@@ -1,6 +1,17 @@
import { randomInt } from "crypto";
const DEFAULT_PASSWORD_REQUIREMENTS = {
type TPasswordRequirements = {
length: number;
required: {
lowercase: number;
uppercase: number;
digits: number;
symbols: number;
};
allowedSymbols?: string;
};
const DEFAULT_PASSWORD_REQUIREMENTS: TPasswordRequirements = {
length: 48,
required: {
lowercase: 1,
@@ -11,9 +22,9 @@ const DEFAULT_PASSWORD_REQUIREMENTS = {
allowedSymbols: "-_.~!*"
};
export const generatePassword = () => {
export const generatePassword = (passwordRequirements?: TPasswordRequirements) => {
try {
const { length, required, allowedSymbols } = DEFAULT_PASSWORD_REQUIREMENTS;
const { length, required, allowedSymbols } = passwordRequirements ?? DEFAULT_PASSWORD_REQUIREMENTS;
const chars = {
lowercase: "abcdefghijklmnopqrstuvwxyz",

View File

@@ -1858,6 +1858,16 @@ export const AppConnections = {
instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).",
accessToken: "The access token to use to connect with Windmill."
},
LDAP: {
provider: "The type of LDAP provider. Determines provider-specific behaviors.",
url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').",
dn: "The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com').",
password: "The password to bind with for authentication.",
sslRejectUnauthorized:
"Whether or not to reject unauthorized SSL certificates (true/false) when using ldaps://. Set to false only in test environments.",
sslCertificate:
"The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate."
},
TEAMCITY: {
instanceUrl: "The TeamCity instance URL to connect with.",
accessToken: "The access token to use to connect with TeamCity."
@@ -2069,6 +2079,22 @@ export const SecretRotations = {
AUTH0_CLIENT_SECRET: {
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
},
LDAP_PASSWORD: {
dn: "The Distinguished Name (DN) of the principal to rotate the password for."
},
GENERAL: {
PASSWORD_REQUIREMENTS: {
base: "The password requirements to use when generating the new password.",
length: "The length of the password to generate.",
required: {
digits: "The amount of digits to require in the generated password.",
lowercase: "The amount of lowercase characters to require in the generated password.",
uppercase: "The amount of uppercase characters to require in the generated password.",
symbols: "The amount of symbols to require in the generated password."
},
allowedSymbols: 'The allowed symbols to use in the generated password (defaults to "-_.~!*").'
}
},
AWS_IAM_USER_SECRET: {
userName: "The name of the client to rotate credentials for.",
region: "The AWS region the client is present in."
@@ -2083,6 +2109,10 @@ export const SecretRotations = {
clientId: "The name of the secret that the client ID will be mapped to.",
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
},
LDAP_PASSWORD: {
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.",
password: "The name of the secret that the rotated password will be mapped to."
},
AWS_IAM_USER_SECRET: {
accessKeyId: "The name of the secret that the access key ID will be mapped to.",
secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to."

View File

@@ -0,0 +1,3 @@
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]+=[^,+="<>#;\\\\]+)*))*)?$/;

View File

@@ -28,6 +28,7 @@ import {
HumanitecConnectionListItemSchema,
SanitizedHumanitecConnectionSchema
} from "@app/services/app-connection/humanitec";
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import {
PostgresConnectionListItemSchema,
@@ -64,6 +65,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedCamundaConnectionSchema.options,
...SanitizedWindmillConnectionSchema.options,
...SanitizedAuth0ConnectionSchema.options,
...SanitizedLdapConnectionSchema.options,
...SanitizedTeamCityConnectionSchema.options
]);
@@ -82,6 +84,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
CamundaConnectionListItemSchema,
WindmillConnectionListItemSchema,
Auth0ConnectionListItemSchema,
LdapConnectionListItemSchema,
TeamCityConnectionListItemSchema
]);

View File

@@ -1,6 +1,6 @@
import { registerAuth0ConnectionRouter } from "@app/server/routes/v1/app-connection-routers/auth0-connection-router";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
import { registerAwsConnectionRouter } from "./aws-connection-router";
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
@@ -9,6 +9,7 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
@@ -34,5 +35,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Camunda]: registerCamundaConnectionRouter,
[AppConnection.Windmill]: registerWindmillConnectionRouter,
[AppConnection.Auth0]: registerAuth0ConnectionRouter,
[AppConnection.LDAP]: registerLdapConnectionRouter,
[AppConnection.TeamCity]: registerTeamCityConnectionRouter
};

View File

@@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateLdapConnectionSchema,
SanitizedLdapConnectionSchema,
UpdateLdapConnectionSchema
} from "@app/services/app-connection/ldap";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerLdapConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.LDAP,
server,
sanitizedResponseSchema: SanitizedLdapConnectionSchema,
createSchema: CreateLdapConnectionSchema,
updateSchema: UpdateLdapConnectionSchema
});
};

View File

@@ -13,6 +13,7 @@ export enum AppConnection {
Camunda = "camunda",
Windmill = "windmill",
Auth0 = "auth0",
LDAP = "ldap",
TeamCity = "teamcity"
}

View File

@@ -41,6 +41,7 @@ import {
HumanitecConnectionMethod,
validateHumanitecConnectionCredentials
} from "./humanitec";
import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap";
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import {
@@ -77,6 +78,7 @@ export const listAppConnectionOptions = () => {
getCamundaConnectionListItem(),
getWindmillConnectionListItem(),
getAuth0ConnectionListItem(),
getLdapConnectionListItem(),
getTeamCityConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -142,6 +144,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator
};
@@ -178,6 +181,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "Access Token";
case Auth0ConnectionMethod.ClientCredentials:
return "Client Credentials";
case LdapConnectionMethod.SimpleBind:
return "Simple Bind";
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -223,5 +228,6 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
[AppConnection.TeamCity]: platformManagedCredentialsNotSupported
};

View File

@@ -15,5 +15,6 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Camunda]: "Camunda",
[AppConnection.Windmill]: "Windmill",
[AppConnection.Auth0]: "Auth0",
[AppConnection.LDAP]: "LDAP",
[AppConnection.TeamCity]: "TeamCity"
};

View File

@@ -43,6 +43,7 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github";
import { githubConnectionService } from "./github/github-connection-service";
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
@@ -77,6 +78,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
[AppConnection.TeamCity]: ValidateTeamCityConnectionCredentialsSchema
};

View File

@@ -57,6 +57,12 @@ import {
THumanitecConnectionInput,
TValidateHumanitecConnectionCredentialsSchema
} from "./humanitec";
import {
TLdapConnection,
TLdapConnectionConfig,
TLdapConnectionInput,
TValidateLdapConnectionCredentialsSchema
} from "./ldap";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import {
TPostgresConnection,
@@ -103,6 +109,7 @@ export type TAppConnection = { id: string } & (
| TCamundaConnection
| TWindmillConnection
| TAuth0Connection
| TLdapConnection
| TTeamCityConnection
);
@@ -125,6 +132,7 @@ export type TAppConnectionInput = { id: string } & (
| TCamundaConnectionInput
| TWindmillConnectionInput
| TAuth0ConnectionInput
| TLdapConnectionInput
| TTeamCityConnectionInput
);
@@ -153,6 +161,7 @@ export type TAppConnectionConfig =
| TCamundaConnectionConfig
| TWindmillConnectionConfig
| TAuth0ConnectionConfig
| TLdapConnectionConfig
| TTeamCityConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
@@ -170,6 +179,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateVercelConnectionCredentialsSchema
| TValidateWindmillConnectionCredentialsSchema
| TValidateAuth0ConnectionCredentialsSchema
| TValidateLdapConnectionCredentialsSchema
| TValidateTeamCityConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {

View File

@@ -0,0 +1,4 @@
export * from "./ldap-connection-enums";
export * from "./ldap-connection-fns";
export * from "./ldap-connection-schemas";
export * from "./ldap-connection-types";

View File

@@ -0,0 +1,7 @@
export enum LdapConnectionMethod {
SimpleBind = "simple-bind"
}
export enum LdapProvider {
ActiveDirectory = "active-directory"
}

View File

@@ -0,0 +1,102 @@
import ldap from "ldapjs";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { LdapConnectionMethod } from "./ldap-connection-enums";
import { TLdapConnectionConfig } from "./ldap-connection-types";
export const getLdapConnectionListItem = () => {
return {
name: "LDAP" as const,
app: AppConnection.LDAP as const,
methods: Object.values(LdapConnectionMethod) as [LdapConnectionMethod.SimpleBind]
};
};
const LDAP_TIMEOUT = 15_000;
export const getLdapConnectionClient = async ({
url,
dn,
password,
sslCertificate,
sslRejectUnauthorized = true
}: TLdapConnectionConfig["credentials"]) => {
await blockLocalAndPrivateIpAddresses(url);
const isSSL = url.startsWith("ldaps");
return new Promise<ldap.Client>((resolve, reject) => {
const client = ldap.createClient({
url,
timeout: LDAP_TIMEOUT,
connectTimeout: LDAP_TIMEOUT,
tlsOptions: isSSL
? {
rejectUnauthorized: sslRejectUnauthorized,
ca: sslCertificate ? [sslCertificate] : undefined
}
: undefined
});
client.on("error", (err: Error) => {
logger.error(err, "LDAP Error");
client.destroy();
reject(new Error(`Provider Error - ${err.message}`));
});
client.on("connectError", (err: Error) => {
logger.error(err, "LDAP Connection Error");
client.destroy();
reject(new Error(`Provider Connect Error - ${err.message}`));
});
client.on("connectRefused", (err: Error) => {
logger.error(err, "LDAP Connection Refused");
client.destroy();
reject(new Error(`Provider Connection Refused - ${err.message}`));
});
client.on("connectTimeout", (err: Error) => {
logger.error(err, "LDAP Connection Timeout");
client.destroy();
reject(new Error(`Provider Connection Timeout - ${err.message}`));
});
client.on("connect", () => {
client.bind(dn, password, (err) => {
if (err) {
logger.error(err, "LDAP Bind Error");
reject(new Error(`Bind Error: ${err.message}`));
client.destroy();
}
resolve(client);
});
});
});
};
export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => {
let client: ldap.Client | undefined;
try {
client = await getLdapConnectionClient(credentials);
// this shouldn't occur as handle connection error events in client but here as fallback
if (!client.connected) {
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
}
return credentials;
} catch (e: unknown) {
throw new BadRequestError({
message: `Unable to validate connection: ${(e as Error).message || "verify credentials"}`
});
} finally {
client?.destroy();
}
};

View File

@@ -0,0 +1,93 @@
import RE2 from "re2";
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { DistinguishedNameRegex } from "@app/lib/regex";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums";
export const LdapConnectionSimpleBindCredentialsSchema = z.object({
provider: z.nativeEnum(LdapProvider).describe(AppConnections.CREDENTIALS.LDAP.provider),
url: z
.string()
.trim()
.min(1, "URL required")
.regex(new RE2(/^ldaps?:\/\//))
.describe(AppConnections.CREDENTIALS.LDAP.url),
dn: z
.string()
.trim()
.regex(new RE2(DistinguishedNameRegex), "Invalid DN format, ie; CN=user,OU=users,DC=example,DC=com")
.min(1, "Distinguished Name (DN) required")
.describe(AppConnections.CREDENTIALS.LDAP.dn),
password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password),
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized),
sslCertificate: z
.string()
.trim()
.transform((value) => value || undefined)
.optional()
.describe(AppConnections.CREDENTIALS.LDAP.sslCertificate)
});
const BaseLdapConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.LDAP)
});
export const LdapConnectionSchema = z.intersection(
BaseLdapConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(LdapConnectionMethod.SimpleBind),
credentials: LdapConnectionSimpleBindCredentialsSchema
})
])
);
export const SanitizedLdapConnectionSchema = z.discriminatedUnion("method", [
BaseLdapConnectionSchema.extend({
method: z.literal(LdapConnectionMethod.SimpleBind),
credentials: LdapConnectionSimpleBindCredentialsSchema.pick({
provider: true,
url: true,
dn: true,
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);
export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(LdapConnectionMethod.SimpleBind).describe(AppConnections.CREATE(AppConnection.LDAP).method),
credentials: LdapConnectionSimpleBindCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.LDAP).credentials
)
})
]);
export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP)
);
export const UpdateLdapConnectionSchema = z
.object({
credentials: LdapConnectionSimpleBindCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.LDAP).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP));
export const LdapConnectionListItemSchema = z.object({
name: z.literal("LDAP"),
app: z.literal(AppConnection.LDAP),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(LdapConnectionMethod).array()
});

View File

@@ -0,0 +1,22 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateLdapConnectionSchema,
LdapConnectionSchema,
ValidateLdapConnectionCredentialsSchema
} from "./ldap-connection-schemas";
export type TLdapConnection = z.infer<typeof LdapConnectionSchema>;
export type TLdapConnectionInput = z.infer<typeof CreateLdapConnectionSchema> & {
app: AppConnection.LDAP;
};
export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema;
export type TLdapConnectionConfig = DiscriminativePick<TLdapConnection, "method" | "app" | "credentials"> & {
orgId: string;
};

View File

@@ -31,7 +31,8 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [
port: true,
username: true,
sslEnabled: true,
sslRejectUnauthorized: true
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);

View File

@@ -29,7 +29,8 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method",
port: true,
username: true,
sslEnabled: true,
sslRejectUnauthorized: true
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);

View File

@@ -12,6 +12,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -39,7 +40,6 @@ import {
AuthTokenType,
MfaMethod
} from "./auth-type";
import { removeTrailingSlash } from "@app/lib/fn";
type TAuthLoginServiceFactoryDep = {
userDAL: TUserDALFactory;

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/ldap/available"
---

View File

@@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/ldap"
---
<Note>
Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain
the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/ldap/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/ldap/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/ldap/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/ldap"
---

View File

@@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/ldap/{connectionId}"
---
<Note>
Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain
the required credentials.
</Note>

View File

@@ -0,0 +1,9 @@
---
title: "Create"
openapi: "POST /api/v2/secret-rotations/ldap-password"
---
<Note>
Check out the configuration docs for [LDAP Password Rotations](/documentation/platform/secret-rotation/ldap-password) to learn how to obtain the
required parameters.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secret-rotations/ldap-password/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v2/secret-rotations/ldap-password/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v2/secret-rotations/ldap-password/rotation-name/{rotationName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get Credentials by ID"
openapi: "GET /api/v2/secret-rotations/ldap-password/{rotationId}/generated-credentials"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-rotations/ldap-password"
---

View File

@@ -0,0 +1,4 @@
---
title: "Rotate Secrets"
openapi: "POST /api/v2/secret-rotations/ldap-password/{rotationId}/rotate-secrets"
---

View File

@@ -0,0 +1,9 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-rotations/ldap-password/{rotationId}"
---
<Note>
Check out the configuration docs for [LDAP Rotations](/documentation/platform/secret-rotation/ldap-password) to learn how to obtain the
required parameters.
</Note>

View File

@@ -1,5 +1,5 @@
---
title: "Auth0 Client Secret"
title: "Auth0 Client Secret Rotation"
description: "Learn how to automatically rotate Auth0 Client Secrets."
---

View File

@@ -0,0 +1,173 @@
---
title: "LDAP Password Rotation"
description: "Learn how to automatically rotate LDAP passwords."
---
<Note>
Due to how LDAP passwords are rotated, retired credentials will not be able to
authenticate with the LDAP provider during their [inactive period](./overview#how-rotation-works).
This is a limitation of the LDAP provider and cannot be
rectified by Infisical.
</Note>
## Prerequisites
- Create an [LDAP Connection](/integrations/app-connections/ldap) with the **Secret Rotation** requirements
## Create an LDAP Password Rotation in Infisical
<Tabs>
<Tab title="Infisical UI">
1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown.
![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png)
2. Select the **LDAP Password** option.
![Select LDAP Password](/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png)
3. Select the **LDAP Connection** to use and configure the rotation behavior. Then click **Next**.
![Rotation Configuration](/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png)
- **LDAP Connection** - the connection that will perform the rotation of the configured DN's password.
<Note>
LDAP Password Rotations require an LDAP Connection that uses ldaps:// protocol.
</Note>
- **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation.
- **Rotate At** - the local time of day when rotation should occur once the interval has elapsed.
- **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation.
<Note>
Due to LDAP Password Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option.
</Note>
4. Specify the Distinguished Name (DN) of the principal whose password you want to rotate and configure the password requirements. Then click **Next**.
![Rotation Parameters](/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png)
5. Specify the secret names that the client credentials should be mapped to. Then click **Next**.
![Rotation Secrets Mapping](/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png)
- **DN** - the name of the secret that the principal's Distinguished Name (DN) will be mapped to.
- **Password** - the name of the secret that the rotated password will be mapped to.
6. Give your rotation a name and description (optional). Then click **Next**.
![Rotation Details](/images/secret-rotations-v2/ldap-password/ldap-password-details.png)
- **Name** - the name of the secret rotation configuration. Must be slug-friendly.
- **Description** (optional) - a description of this rotation configuration.
7. Review your configuration, then click **Create Secret Rotation**.
![Rotation Review](/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png)
8. Your **LDAP Password** credentials are now available for use via the mapped secrets.
![Rotation Created](/images/secret-rotations-v2/ldap-password/ldap-password-created.png)
</Tab>
<Tab title="API">
To create an LDAP Password Rotation, make an API request to the [Create LDAP
Password Rotation](/api-reference/endpoints/secret-rotations/ldap-password/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://us.infisical.com/api/v2/secret-rotations/ldap-password \
--header 'Content-Type: application/json' \
--data '{
"name": "my-ldap-rotation",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "my ldap password rotation",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"environment": "dev",
"secretPath": "/",
"isAutoRotationEnabled": false,
"rotationInterval": 30,
"rotateAtUtc": {
"hours": 0,
"minutes": 0
},
"parameters": {
"dn": "CN=John,CN=Users,DC=example,DC=com",
"passwordRequirements": {
"length": 48,
"required": {
"digits": 2,
"lowercase": 2,
"uppercase": 2,
"symbols": 2
},
"allowedSymbols": "-_.~!*"
}
},
"secretsMapping": {
"dn": "LDAP_DN",
"password": "LDAP_PASSWORD"
}
}'
```
<Note>
Due to LDAP Password Rotations rotating a single credential set, auto-rotation may result in service interruptions. If you need to ensure service continuity, we recommend disabling this option.
</Note>
### Sample response
```bash Response
{
"secretRotation": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-ldap-rotation",
"description": "my ldap password rotation",
"secretsMapping": {
"dn": "LDAP_DN",
"password": "LDAP_PASSWORD"
},
"isAutoRotationEnabled": false,
"activeIndex": 0,
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"rotationInterval": 30,
"rotationStatus": "success",
"lastRotationAttemptedAt": "2023-11-07T05:31:56Z",
"lastRotatedAt": "2023-11-07T05:31:56Z",
"lastRotationJobId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"nextRotationAt": "2023-11-07T05:31:56Z",
"connection": {
"app": "ldap",
"name": "my-ldap-connection",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"environment": {
"slug": "dev",
"name": "Development",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"folder": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"path": "/"
},
"rotateAtUtc": {
"hours": 0,
"minutes": 0
},
"lastRotationMessage": null,
"type": "ldap-password",
"parameters": {
"dn": "CN=John,CN=Users,DC=example,DC=com",
"passwordRequirements": {
"length": 48,
"required": {
"digits": 2,
"lowercase": 2,
"uppercase": 2,
"symbols": 2
},
"allowedSymbols": "-_.~!*"
}
}
}
}
```
</Tab>
</Tabs>

View File

@@ -1,5 +1,5 @@
---
title: "Microsoft SQL Server Credentials"
title: "Microsoft SQL Server Credentials Rotation"
description: "Learn how to automatically rotate Microsoft SQL Server credentials."
---

View File

@@ -1,5 +1,5 @@
---
title: "PostgreSQL Credentials"
title: "PostgreSQL Credentials Rotation"
description: "Learn how to automatically rotate PostgreSQL credentials."
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 809 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

View File

@@ -0,0 +1,96 @@
---
title: "LDAP Connection"
description: "Learn how to configure an LDAP Connection for Infisical."
---
Infisical supports the use of [Simple Binding](https://ldap.com/the-ldap-bind-operation) to connect with your LDAP provider.
## Prerequisites
You will need the following information to establish an LDAP connection:
- **LDAP URL** - The LDAP/LDAPS URL to connect to (e.g., ldap://domain-or-ip:389 or ldaps://domain-or-ip:636)
- **Binding DN** - The Distinguished Name (DN) of the principal to bind with (e.g., 'CN=John,CN=Users,DC=example,DC=com')
- **Binding Password** - The password to bind with for authentication
- **CA Certificate** - The SSL certificate (PEM format) to use for secure connection when using ldaps:// with a self-signed certificate
Depending on how you intend to use your LDAP connection, there may be additional requirements:
<Tabs>
<Tab title="Secret Rotation">
<Note>
For Password Rotation, the following requirements must additionally be met:
- You must use an LDAPS connection
- The binding user must either have:
- Permission to change other users passwords if rotating directory users' passwords
- Permission to update their own password if rotating their personal password
</Note>
</Tab>
</Tabs>
## Setup LDAP Connection in Infisical
<Tabs>
<Tab title="Infisical UI">
1. Navigate to the App Connections tab on the Organization Settings page.
![App Connections Tab](/images/app-connections/general/add-connection.png)
2. Select the **LDAP Connection** option.
![Select LDAP Connection](/images/app-connections/ldap/select-ldap-connection.png)
3. Select the **Simple Bind** method option and provide the details obtained from the previous section and press **Connect to Provider**.
![Create LDAP Connection](/images/app-connections/ldap/create-simple-bind-method.png)
4. Your **LDAP Connection** is now available for use.
![Assume Role LDAP Connection](/images/app-connections/ldap/simple-bind-connection.png)
</Tab>
<Tab title="API">
To create an LDAP Connection, make an API request to the [Create LDAP
Connection](/api-reference/endpoints/app-connections/ldap/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/app-connections/ldap \
--header 'Content-Type: application/json' \
--data '{
"name": "my-ldap-connection",
"method": "simple-bind",
"credentials": {
"provider": "active-directory",
"url": "ldaps://domain-or-ip:636",
"dn": "CN=John,CN=Users,DC=example,DC=com",
"password": "<your-secure-password>",
"sslRejectUnauthorized": true,
"sslCertificate": "..."
}
}'
```
### Sample response
```bash Response
{
"appConnection": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-ldap-connection",
"version": 1,
"orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"app": "ldap",
"method": "simple-bind",
"credentials": {
"provider": "active-directory",
"url": "ldaps://domain-or-ip:636",
"dn": "CN=John,CN=Users,DC=example,DC=com",
"sslRejectUnauthorized": true,
"sslCertificate": "..."
}
}
}
```
</Tab>
</Tabs>

View File

@@ -180,8 +180,9 @@
"documentation/platform/secret-rotation/overview",
"documentation/platform/secret-rotation/auth0-client-secret",
"documentation/platform/secret-rotation/aws-iam-user-secret",
"documentation/platform/secret-rotation/postgres-credentials",
"documentation/platform/secret-rotation/mssql-credentials"
"documentation/platform/secret-rotation/ldap-password",
"documentation/platform/secret-rotation/mssql-credentials",
"documentation/platform/secret-rotation/postgres-credentials"
]
},
{
@@ -433,6 +434,7 @@
"integrations/app-connections/gcp",
"integrations/app-connections/github",
"integrations/app-connections/humanitec",
"integrations/app-connections/ldap",
"integrations/app-connections/mssql",
"integrations/app-connections/postgres",
"integrations/app-connections/teamcity",
@@ -883,6 +885,19 @@
"api-reference/endpoints/secret-rotations/aws-iam-user-secret/update"
]
},
{
"group": "LDAP Password",
"pages": [
"api-reference/endpoints/secret-rotations/ldap-password/create",
"api-reference/endpoints/secret-rotations/ldap-password/delete",
"api-reference/endpoints/secret-rotations/ldap-password/get-by-id",
"api-reference/endpoints/secret-rotations/ldap-password/get-by-name",
"api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id",
"api-reference/endpoints/secret-rotations/ldap-password/list",
"api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets",
"api-reference/endpoints/secret-rotations/ldap-password/update"
]
},
{
"group": "Microsoft SQL Server Credentials",
"pages": [
@@ -1035,6 +1050,18 @@
"api-reference/endpoints/app-connections/humanitec/delete"
]
},
{
"group": "LDAP",
"pages": [
"api-reference/endpoints/app-connections/ldap/list",
"api-reference/endpoints/app-connections/ldap/available",
"api-reference/endpoints/app-connections/ldap/get-by-id",
"api-reference/endpoints/app-connections/ldap/get-by-name",
"api-reference/endpoints/app-connections/ldap/create",
"api-reference/endpoints/app-connections/ldap/update",
"api-reference/endpoints/app-connections/ldap/delete"
]
},
{
"group": "Microsoft SQL Server",
"pages": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -1,8 +1,6 @@
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { TAuth0ClientSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/auth0-client-secret-rotation";
import { ViewRotationGeneratedCredentialsDisplay } from "./shared";
import { CredentialDisplay, ViewRotationGeneratedCredentialsDisplay } from "./shared";
type Props = {
generatedCredentialsResponse: TAuth0ClientSecretRotationGeneratedCredentialsResponse;
@@ -17,7 +15,6 @@ export const ViewAuth0ClientSecretRotationGeneratedCredentials = ({
const inactiveCredentials = generatedCredentials[inactiveIndex];
return (
<>
<ViewRotationGeneratedCredentialsDisplay
activeCredentials={
<>
@@ -36,21 +33,5 @@ export const ViewAuth0ClientSecretRotationGeneratedCredentials = ({
</>
}
/>
<NoticeBannerV2 title="Auth0 Retired Credentials Behavior">
<p className="text-sm text-mineshaft-300">
Due to how Auth0 client secrets are rotated, retired credentials will not be able to
authenticate with Auth0 during their{" "}
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-rotation/overview#how-rotation-works"
rel="noopener noreferrer"
className="underline decoration-primary underline-offset-2 hover:text-mineshaft-200"
>
inactive period
</a>
. This is a limitation of the Auth0 platform and cannot be rectified by Infisical.
</p>
</NoticeBannerV2>
</>
);
};

View File

@@ -0,0 +1,41 @@
import { TLdapPasswordRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation";
import { CredentialDisplay, ViewRotationGeneratedCredentialsDisplay } from "./shared";
type Props = {
generatedCredentialsResponse: TLdapPasswordRotationGeneratedCredentialsResponse;
};
export const ViewLdapPasswordRotationGeneratedCredentials = ({
generatedCredentialsResponse: { generatedCredentials, activeIndex }
}: Props) => {
const inactiveIndex = activeIndex === 0 ? 1 : 0;
const activeCredentials = generatedCredentials[activeIndex];
const inactiveCredentials = generatedCredentials[inactiveIndex];
return (
<ViewRotationGeneratedCredentialsDisplay
activeCredentials={
<>
<CredentialDisplay label="Distinguished Name (DN)">
{activeCredentials?.dn}
</CredentialDisplay>
<CredentialDisplay isSensitive label="Password">
{activeCredentials?.password}
</CredentialDisplay>
</>
}
inactiveCredentials={
<>
<CredentialDisplay label="Distinguished Name (DN)">
{inactiveCredentials?.dn}
</CredentialDisplay>
<CredentialDisplay isSensitive label="Password">
{inactiveCredentials?.password}
</CredentialDisplay>
</>
}
/>
);
};

View File

@@ -4,8 +4,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { ViewAuth0ClientSecretRotationGeneratedCredentials } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials";
import { ViewLdapPasswordRotationGeneratedCredentials } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials";
import { Modal, ModalContent, Spinner } from "@app/components/v2";
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import {
IS_ROTATION_DUAL_CREDENTIALS,
SECRET_ROTATION_CONNECTION_MAP,
SECRET_ROTATION_MAP
} from "@app/helpers/secretRotationsV2";
import {
SecretRotation,
TSecretRotationV2,
@@ -68,6 +75,13 @@ const Content = ({ secretRotation }: ContentProps) => {
/>
);
break;
case SecretRotation.LdapPassword:
Component = (
<ViewLdapPasswordRotationGeneratedCredentials
generatedCredentialsResponse={generatedCredentialsResponse}
/>
);
break;
case SecretRotation.AwsIamUserSecret:
Component = (
<ViewAwsIamUserSecretRotationGeneratedCredentials
@@ -79,9 +93,28 @@ const Content = ({ secretRotation }: ContentProps) => {
throw new Error("Unhandled View Generated Credential Rotation Type");
}
const appName = APP_CONNECTION_MAP[SECRET_ROTATION_CONNECTION_MAP[type]].name;
return (
<div className="flex flex-col gap-y-4">
{Component}
{!IS_ROTATION_DUAL_CREDENTIALS[type] && (
<NoticeBannerV2 title={`${appName} Retired Credentials Behavior`}>
<p className="text-sm text-mineshaft-300">
Due to {SECRET_ROTATION_MAP[type].name} Rotations utilizing a single credential set,
retired credentials will not be able to authenticate with {appName} during their{" "}
<a
target="_blank"
href="https://infisical.com/docs/documentation/platform/secret-rotation/overview#how-rotation-works"
rel="noopener noreferrer"
className="underline decoration-primary underline-offset-2 hover:text-mineshaft-200"
>
inactive period
</a>
. This is a limitation of {appName} and cannot be rectified by Infisical.
</p>
</NoticeBannerV2>
)}
{nextRotationAt && (
<div className="flex items-center gap-x-1.5 text-sm text-mineshaft-200">
<FontAwesomeIcon icon={faRotate} className="text-mineshaft-400" />

View File

@@ -0,0 +1,169 @@
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 { SecretRotation } from "@app/hooks/api/secretRotationsV2";
export const LdapPasswordRotationParametersFields = () => {
const { control } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.LdapPassword;
}
>();
return (
<>
<Controller
name="parameters.dn"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Distinguished Name (DN)"
>
<Input
value={value}
onChange={onChange}
placeholder="CN=John,OU=Users,DC=example,DC=com"
/>
</FormControl>
)}
/>
<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">
<Controller
control={control}
name="parameters.passwordRequirements.length"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.length}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Password Length"
isError={Boolean(error)}
errorText={error?.message}
helperText="The length of the password to generate"
>
<Input
type="number"
min={1}
max={250}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.required.digits"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.digits}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Digit Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of digits"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.required.lowercase"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.lowercase}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Lowercase Character Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of lowercase characters"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.required.uppercase"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.uppercase}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Uppercase Character Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of uppercase characters"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.required.symbols"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.required.symbols}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Symbol Count"
isError={Boolean(error)}
errorText={error?.message}
helperText="Minimum number of symbols"
>
<Input
type="number"
min={0}
size="sm"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
name="parameters.passwordRequirements.allowedSymbols"
defaultValue={DEFAULT_PASSWORD_REQUIREMENTS.allowedSymbols}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Symbols"
isError={Boolean(error)}
errorText={error?.message}
helperText="Symbols to use in generated password"
>
<Input
placeholder="-_.~!*"
size="sm"
{...field}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div>
</div>
</>
);
};

View File

@@ -5,12 +5,14 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { TSecretRotationV2Form } from "../schemas";
import { Auth0ClientSecretRotationParametersFields } from "./Auth0ClientSecretRotationParametersFields";
import { AwsIamUserSecretRotationParametersFields } from "./AwsIamUserSecretRotationParametersFields";
import { LdapPasswordRotationParametersFields } from "./LdapPasswordRotationParametersFields";
import { SqlCredentialsRotationParametersFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields
};

View File

@@ -0,0 +1,29 @@
import { useFormContext } from "react-hook-form";
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
import { GenericFieldLabel } from "@app/components/v2";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { SecretRotationReviewSection } from "./shared";
export const LdapPasswordRotationReviewFields = () => {
const { watch } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.LdapPassword;
}
>();
const [parameters, { dn, password }] = watch(["parameters", "secretsMapping"]);
return (
<>
<SecretRotationReviewSection label="Parameters">
<GenericFieldLabel label="Distinguished Name (DN)">{parameters.dn}</GenericFieldLabel>
</SecretRotationReviewSection>
<SecretRotationReviewSection label="Secrets Mapping">
<GenericFieldLabel label="Distinguished Name (DN)">{dn}</GenericFieldLabel>
<GenericFieldLabel label="Password">{password}</GenericFieldLabel>
</SecretRotationReviewSection>
</>
);
};

View File

@@ -8,12 +8,14 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { Auth0ClientSecretRotationReviewFields } from "./Auth0ClientSecretRotationReviewFields";
import { AwsIamUserSecretRotationReviewFields } from "./AwsIamUserSecretRotationReviewFields";
import { LdapPasswordRotationReviewFields } from "./LdapPasswordRotationReviewFields";
import { SqlCredentialsRotationReviewFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationReviewFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields
};

View File

@@ -0,0 +1,58 @@
import { Controller, useFormContext } from "react-hook-form";
import { TSecretRotationV2Form } from "@app/components/secret-rotations-v2/forms/schemas";
import { FormControl, Input } from "@app/components/v2";
import { SecretRotation, useSecretRotationV2Option } from "@app/hooks/api/secretRotationsV2";
import { SecretsMappingTable } from "./shared";
export const LdapPasswordRotationSecretsMappingFields = () => {
const { control } = useFormContext<
TSecretRotationV2Form & {
type: SecretRotation.LdapPassword;
}
>();
const { rotationOption } = useSecretRotationV2Option(SecretRotation.LdapPassword);
const items = [
{
name: "DN",
input: (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
value={value}
onChange={onChange}
placeholder={rotationOption?.template.secretsMapping.dn}
/>
</FormControl>
)}
control={control}
name="secretsMapping.dn"
/>
)
},
{
name: "Password",
input: (
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
value={value}
onChange={onChange}
placeholder={rotationOption?.template.secretsMapping.password}
/>
</FormControl>
)}
control={control}
name="secretsMapping.password"
/>
)
}
];
return <SecretsMappingTable items={items} />;
};

View File

@@ -5,12 +5,14 @@ import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import { TSecretRotationV2Form } from "../schemas";
import { Auth0ClientSecretRotationSecretsMappingFields } from "./Auth0ClientSecretRotationSecretsMappingFields";
import { AwsIamUserSecretRotationSecretsMappingFields } from "./AwsIamUserSecretRotationSecretsMappingFields";
import { LdapPasswordRotationSecretsMappingFields } from "./LdapPasswordRotationSecretsMappingFields";
import { SqlCredentialsRotationSecretsMappingFields } from "./shared";
const COMPONENT_MAP: Record<SecretRotation, React.FC> = {
[SecretRotation.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields,
[SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields,
[SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields,
[SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields,
[SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields
};

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import { Auth0ClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/auth0-client-secret-rotation-schema";
import { AwsIamUserSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/aws-iam-user-secret-rotation-schema";
import { LdapPasswordRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema";
import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema";
import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema";
@@ -9,6 +10,7 @@ const SecretRotationUnionSchema = z.discriminatedUnion("type", [
PostgresCredentialsRotationSchema,
MsSqlCredentialsRotationSchema,
Auth0ClientSecretRotationSchema,
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema
]);

View File

@@ -0,0 +1,24 @@
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 { SecretRotation } from "@app/hooks/api/secretRotationsV2";
export const LdapPasswordRotationSchema = z
.object({
type: z.literal(SecretRotation.LdapPassword),
parameters: z.object({
dn: z
.string()
.trim()
.regex(DistinguishedNameRegex, "Invalid Distinguished Name format")
.min(1, "Distinguished Name (DN) required"),
passwordRequirements: PasswordRequirementsSchema.optional()
}),
secretsMapping: z.object({
dn: z.string().trim().min(1, "Distinguished Name (DN) required"),
password: z.string().trim().min(1, "Password required")
})
})
.merge(BaseSecretRotationSchema);

View File

@@ -1 +1,2 @@
export * from "./password-requirements-schema";
export * from "./sql-credentials-rotation-schema";

View File

@@ -0,0 +1,47 @@
import { z } from "zod";
export const PasswordRequirementsSchema = z
.object({
length: z
.number()
.min(1, "Password length must be a positive number")
.max(250, "Password length must be less than 250"),
required: z.object({
digits: z.number().min(0, "Digit count must be non-negative"),
lowercase: z.number().min(0, "Lowercase count must be non-negative"),
uppercase: z.number().min(0, "Uppercase count must be non-negative"),
symbols: z.number().min(0, "Symbol count must be non-negative")
}),
allowedSymbols: z
.string()
.regex(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/, "Invalid symbols")
.optional()
.transform((value) => value || "-_.~!*")
})
.refine(
(data) => {
return Object.values(data.required).some((count) => count > 0);
},
{
message: "At least one character type must be required",
path: ["required.digits"]
}
)
.refine(
(data) => {
const total = Object.values(data.required).reduce((sum, count) => sum + count, 0);
return total <= data.length;
},
{ message: "Sum of required characters cannot exceed the total length", path: ["length"] }
);
export const DEFAULT_PASSWORD_REQUIREMENTS = {
length: 48,
required: {
lowercase: 1,
uppercase: 1,
digits: 1,
symbols: 0
},
allowedSymbols: "-_.~!*"
};

View File

@@ -111,7 +111,7 @@ export const TeamCitySyncFields = () => {
onChange(selectedOption?.id ?? "");
}}
options={buildTypes}
isClearable={true}
isClearable
placeholder="Select a build configuration..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}

View File

@@ -1,5 +1,12 @@
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import { faKey, faLock, faPassport, faServer, faUser } from "@fortawesome/free-solid-svg-icons";
import {
faKey,
faLink,
faLock,
faPassport,
faServer,
faUser
} from "@fortawesome/free-solid-svg-icons";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
@@ -12,6 +19,7 @@ import {
GcpConnectionMethod,
GitHubConnectionMethod,
HumanitecConnectionMethod,
LdapConnectionMethod,
MsSqlConnectionMethod,
PostgresConnectionMethod,
TAppConnection,
@@ -45,6 +53,7 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" },
[AppConnection.Windmill]: { name: "Windmill", image: "Windmill.png" },
[AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 },
[AppConnection.LDAP]: { name: "LDAP", image: "LDAP.png", size: 65 },
[AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" }
};
@@ -78,6 +87,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
return { name: "Access Token", icon: faKey };
case Auth0ConnectionMethod.ClientCredentials:
return { name: "Client Credentials", icon: faServer };
case LdapConnectionMethod.SimpleBind:
return { name: "Simple Bind", icon: faLink };
default:
throw new Error(`Unhandled App Connection Method: ${method}`);
}

View File

@@ -20,6 +20,11 @@ export const SECRET_ROTATION_MAP: Record<
image: "Auth0.png",
size: 35
},
[SecretRotation.LdapPassword]: {
name: "LDAP Password",
image: "LDAP.png",
size: 65
},
[SecretRotation.AwsIamUserSecret]: {
name: "AWS IAM User Secret",
image: "Amazon Web Services.png",
@@ -31,6 +36,7 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
};
@@ -39,6 +45,7 @@ export const IS_ROTATION_DUAL_CREDENTIALS: Record<SecretRotation, boolean> = {
[SecretRotation.PostgresCredentials]: true,
[SecretRotation.MsSqlCredentials]: true,
[SecretRotation.Auth0ClientSecret]: false,
[SecretRotation.LdapPassword]: false,
[SecretRotation.AwsIamUserSecret]: true
};

View File

@@ -12,3 +12,6 @@ export const isValidPath = (val: string): boolean => {
const validPathRegex = /^[a-zA-Z0-9-_.:]+(?:\/[a-zA-Z0-9-_.:]+)*$/;
return validPathRegex.test(val);
};
export const DistinguishedNameRegex =
/^(?:(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*)(?:,(?:[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)(?:(?:\\+[a-zA-Z0-9]+=[^,+="<>#;\\\\]+)*))*)?$/;

View File

@@ -13,5 +13,6 @@ export enum AppConnection {
Camunda = "camunda",
Windmill = "windmill",
Auth0 = "auth0",
LDAP = "ldap",
TeamCity = "teamcity"
}

View File

@@ -67,6 +67,10 @@ export type TAuth0ConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Auth0;
};
export type TLdapConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.LDAP;
};
export type TTeamCityConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.TeamCity;
};
@@ -103,5 +107,6 @@ export type TAppConnectionOptionMap = {
[AppConnection.Camunda]: TCamundaConnectionOption;
[AppConnection.Windmill]: TWindmillConnectionOption;
[AppConnection.Auth0]: TAuth0ConnectionOption;
[AppConnection.LDAP]: TLdapConnectionOption;
[AppConnection.TeamCity]: TTeamCityConnectionOption;
};

View File

@@ -9,6 +9,7 @@ import { TDatabricksConnection } from "./databricks-connection";
import { TGcpConnection } from "./gcp-connection";
import { TGitHubConnection } from "./github-connection";
import { THumanitecConnection } from "./humanitec-connection";
import { TLdapConnection } from "./ldap-connection";
import { TMsSqlConnection } from "./mssql-connection";
import { TPostgresConnection } from "./postgres-connection";
import { TTeamCityConnection } from "./teamcity-connection";
@@ -25,6 +26,7 @@ export * from "./databricks-connection";
export * from "./gcp-connection";
export * from "./github-connection";
export * from "./humanitec-connection";
export * from "./ldap-connection";
export * from "./mssql-connection";
export * from "./postgres-connection";
export * from "./teamcity-connection";
@@ -47,6 +49,7 @@ export type TAppConnection =
| TCamundaConnection
| TWindmillConnection
| TAuth0Connection
| TLdapConnection
| TTeamCityConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id">;
@@ -89,5 +92,6 @@ export type TAppConnectionMap = {
[AppConnection.Camunda]: TCamundaConnection;
[AppConnection.Windmill]: TWindmillConnection;
[AppConnection.Auth0]: TAuth0Connection;
[AppConnection.LDAP]: TLdapConnection;
[AppConnection.TeamCity]: TTeamCityConnection;
};

View File

@@ -0,0 +1,21 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
export enum LdapConnectionMethod {
SimpleBind = "simple-bind"
}
export enum LdapConnectionProvider {
ActiveDirectory = "active-directory"
}
export type TLdapConnection = TRootAppConnection & { app: AppConnection.LDAP } & {
method: LdapConnectionMethod.SimpleBind;
credentials: {
provider: LdapConnectionProvider;
url: string;
dn: string;
sslRejectUnauthorized?: boolean;
sslCertificate?: string;
};
};

View File

@@ -2,6 +2,7 @@ export enum SecretRotation {
PostgresCredentials = "postgres-credentials",
MsSqlCredentials = "mssql-credentials",
Auth0ClientSecret = "auth0-client-secret",
LdapPassword = "ldap-password",
AwsIamUserSecret = "aws-iam-user-secret"
}

View File

@@ -9,6 +9,11 @@ import {
TAwsIamUserSecretRotationGeneratedCredentialsResponse,
TAwsIamUserSecretRotationOption
} from "@app/hooks/api/secretRotationsV2/types/aws-iam-user-secret-rotation";
import {
TLdapPasswordRotation,
TLdapPasswordRotationGeneratedCredentialsResponse,
TLdapPasswordRotationOption
} from "@app/hooks/api/secretRotationsV2/types/ldap-password-rotation";
import {
TMsSqlCredentialsRotation,
TMsSqlCredentialsRotationGeneratedCredentialsResponse
@@ -25,6 +30,7 @@ export type TSecretRotationV2 = (
| TPostgresCredentialsRotation
| TMsSqlCredentialsRotation
| TAuth0ClientSecretRotation
| TLdapPasswordRotation
| TAwsIamUserSecretRotation
) & {
secrets: (SecretV3RawSanitized | null)[];
@@ -33,6 +39,7 @@ export type TSecretRotationV2 = (
export type TSecretRotationV2Option =
| TSqlCredentialsRotationOption
| TAuth0ClientSecretRotationOption
| TLdapPasswordRotationOption
| TAwsIamUserSecretRotationOption;
export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] };
@@ -43,6 +50,7 @@ export type TViewSecretRotationGeneratedCredentialsResponse =
| TPostgresCredentialsRotationGeneratedCredentialsResponse
| TMsSqlCredentialsRotationGeneratedCredentialsResponse
| TAuth0ClientSecretRotationGeneratedCredentialsResponse
| TLdapPasswordRotationGeneratedCredentialsResponse
| TAwsIamUserSecretRotationGeneratedCredentialsResponse;
export type TCreateSecretRotationV2DTO = DiscriminativePick<
@@ -90,6 +98,7 @@ export type TSecretRotationOptionMap = {
[SecretRotation.PostgresCredentials]: TSqlCredentialsRotationOption;
[SecretRotation.MsSqlCredentials]: TSqlCredentialsRotationOption;
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationOption;
[SecretRotation.LdapPassword]: TLdapPasswordRotationOption;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption;
};
@@ -97,5 +106,6 @@ export type TSecretRotationGeneratedCredentialsResponseMap = {
[SecretRotation.PostgresCredentials]: TPostgresCredentialsRotationGeneratedCredentialsResponse;
[SecretRotation.MsSqlCredentials]: TMsSqlCredentialsRotationGeneratedCredentialsResponse;
[SecretRotation.Auth0ClientSecret]: TAuth0ClientSecretRotationGeneratedCredentialsResponse;
[SecretRotation.LdapPassword]: TLdapPasswordRotationGeneratedCredentialsResponse;
[SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse;
};

View File

@@ -0,0 +1,37 @@
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretRotation } from "@app/hooks/api/secretRotationsV2";
import {
TSecretRotationV2Base,
TSecretRotationV2GeneratedCredentialsResponseBase
} from "@app/hooks/api/secretRotationsV2/types/shared";
export type TLdapPasswordRotation = TSecretRotationV2Base & {
type: SecretRotation.LdapPassword;
parameters: {
dn: string;
};
secretsMapping: {
dn: string;
password: string;
};
};
export type TLdapPasswordRotationGeneratedCredentials = {
dn: string;
password: string;
};
export type TLdapPasswordRotationGeneratedCredentialsResponse =
TSecretRotationV2GeneratedCredentialsResponseBase<
SecretRotation.LdapPassword,
TLdapPasswordRotationGeneratedCredentials
>;
export type TLdapPasswordRotationOption = {
name: string;
type: SecretRotation.LdapPassword;
connection: AppConnection.LDAP;
template: {
secretsMapping: TLdapPasswordRotation["secretsMapping"];
};
};

View File

@@ -1,13 +1,13 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { AxiosError } from "axios";
import { addSeconds, formatISO } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { SessionStorageKeys } from "@app/const";
import { ROUTE_PATHS } from "@app/const/routes";
import { userKeys } from "@app/hooks/api";
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
import { clearSession, fetchUserDetails, logoutUser } from "@app/hooks/api/users/queries";
import { SessionStorageKeys } from "@app/const";
import { addSeconds, formatISO } from "date-fns";
export const Route = createFileRoute("/_authenticate")({
beforeLoad: async ({ context, location }) => {

View File

@@ -18,6 +18,7 @@ import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm";
import { HumanitecConnectionForm } from "./HumanitecConnectionForm";
import { LdapConnectionForm } from "./LdapConnectionForm";
import { MsSqlConnectionForm } from "./MsSqlConnectionForm";
import { PostgresConnectionForm } from "./PostgresConnectionForm";
import { TeamCityConnectionForm } from "./TeamCityConnectionForm";
@@ -90,6 +91,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
return <WindmillConnectionForm onSubmit={onSubmit} />;
case AppConnection.Auth0:
return <Auth0ConnectionForm onSubmit={onSubmit} />;
case AppConnection.LDAP:
return <LdapConnectionForm onSubmit={onSubmit} />;
case AppConnection.TeamCity:
return <TeamCityConnectionForm onSubmit={onSubmit} />;
default:
@@ -156,8 +159,11 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
return <WindmillConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Auth0:
return <Auth0ConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.LDAP:
return <LdapConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.TeamCity:
return <TeamCityConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
default:
throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`);
}

View File

@@ -0,0 +1,329 @@
import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import {
Button,
FormControl,
Input,
ModalClose,
SecretInput,
Select,
SelectItem,
Switch,
TextArea,
Tooltip
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { DistinguishedNameRegex } from "@app/helpers/string";
import {
LdapConnectionMethod,
LdapConnectionProvider,
TLdapConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TLdapConnection;
onSubmit: (formData: FormData) => Promise<void>;
};
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.LDAP)
});
const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(LdapConnectionMethod.SimpleBind),
credentials: z.object({
provider: z.nativeEnum(LdapConnectionProvider),
url: z
.string()
.regex(/^ldaps?:\/\//, 'Must start with "ldaps://" or "ldap://"')
.url()
.trim()
.min(1, "LDAP URL required"),
dn: z
.string()
.trim()
.regex(DistinguishedNameRegex, "Invalid Distinguished Name format")
.min(1, "Distinguished Name (DN) required"),
password: z.string().trim().min(1, "Password required"),
sslRejectUnauthorized: z.boolean(),
sslCertificate: z
.string()
.trim()
.transform((value) => value || undefined)
.optional()
})
})
]);
type FormData = z.infer<typeof formSchema>;
export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {
const isUpdate = Boolean(appConnection);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.LDAP,
method: LdapConnectionMethod.SimpleBind,
credentials: {
provider: LdapConnectionProvider.ActiveDirectory,
url: "",
dn: "",
password: "",
sslRejectUnauthorized: true,
sslCertificate: undefined
}
}
});
const {
handleSubmit,
control,
formState: { isSubmitting, isDirty },
watch
} = form;
const selectedProvider = watch("credentials.provider");
const sslEnabled = watch("credentials.url")?.startsWith("ldaps://") ?? false;
return (
<FormProvider {...form}>
<form
onSubmit={(e) => {
setSelectedTabIndex(0);
handleSubmit(onSubmit)(e);
}}
>
{!isUpdate && <GenericAppConnectionsFields />}
<div className="grid grid-cols-2 items-center gap-2">
<Controller
name="method"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText={`The method you would like to use to connect with ${
APP_CONNECTION_MAP[AppConnection.LDAP].name
}. This field cannot be changed after creation.`}
errorText={error?.message}
isError={Boolean(error?.message)}
label="Method"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(LdapConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{getAppConnectionMethodDetails(method).name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
<Controller
name="credentials.provider"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="LDAP Provider"
>
<Select
isDisabled={isUpdate}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500 capitalize"
position="popper"
dropdownContainerClassName="max-w-none"
>
{Object.values(LdapConnectionProvider).map((provider) => {
return (
<SelectItem value={provider} className="capitalize" key={provider}>
{provider.replace("-", " ")}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
</div>
<Tab.Group selectedIndex={selectedTabIndex} onChange={setSelectedTabIndex}>
<Tab.List
className={`-pb-1 ${selectedTabIndex === 1 ? "mb-3" : "mb-6"} w-full border-b-2 border-mineshaft-600`}
>
<Tab
className={({ selected }) =>
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
: "text-bunker-300"
}`
}
>
Configuration
</Tab>
<Tab
className={({ selected }) =>
`w-30 -mb-[0.14rem] px-4 py-2 text-sm font-medium outline-none disabled:opacity-60 ${
selected
? "border-b-2 border-mineshaft-300 text-mineshaft-200"
: "text-bunker-300"
}`
}
>
SSL ({sslEnabled ? "Enabled" : "Disabled"})
</Tab>
</Tab.List>
{selectedTabIndex === 1 && (
<div className="mb-2 text-xs text-mineshaft-300">Requires ldaps:// URL</div>
)}
<Tab.Panels className="mb-4 rounded border border-mineshaft-600 bg-mineshaft-700/70 p-3 pb-0">
<Tab.Panel>
<Controller
name="credentials.url"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="LDAP URL"
>
<Input {...field} placeholder="ldap://domain-or-ip:389" />
</FormControl>
)}
/>
<div className="grid grid-cols-2 gap-2">
<Controller
name="credentials.dn"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Binding Distinguished Name (DN)"
>
<Input {...field} placeholder="CN=John,OU=Users,DC=example,DC=com" />
</FormControl>
)}
/>
<Controller
name="credentials.password"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Binding Password"
>
<SecretInput
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormControl>
)}
/>
</div>
</Tab.Panel>
<Tab.Panel>
<Controller
name="credentials.sslCertificate"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
className={sslEnabled ? "" : "opacity-50"}
label="SSL Certificate"
isOptional
>
<TextArea
className="h-[3.6rem] !resize-none"
{...field}
isDisabled={!sslEnabled}
/>
</FormControl>
)}
/>
<Controller
name="credentials.sslRejectUnauthorized"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
className={sslEnabled ? "" : "opacity-50"}
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="ssl-reject-unauthorized"
thumbClassName="bg-mineshaft-800"
isChecked={sslEnabled ? value : false}
onCheckedChange={onChange}
isDisabled={!sslEnabled}
>
<p className="w-[9.5rem]">
Reject Unauthorized
<Tooltip
className="max-w-md"
content={
<p>
If enabled, Infisical will only connect to the server if it has a
valid, trusted SSL certificate.
</p>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
</FormControl>
)}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
<div className="mt-8 flex items-center">
<Button
className="mr-4 capitalize"
size="sm"
type="submit"
colorSchema="secondary"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
{isUpdate ? "Update Credentials" : `Connect to ${selectedProvider.replace("-", " ")}`}
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -125,7 +125,7 @@ export const AppConnectionRow = ({
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={isIdCopied ? faCheck : faCopy} />}
onClick={() => handleCopyId()}

View File

@@ -135,7 +135,6 @@ export const SecretOverviewSecretRotationRow = ({
<img
src={`/images/integrations/${image}`}
style={{
height: "11px",
width: "11px"
}}
alt={`${rotationType} logo`}

View File

@@ -55,7 +55,6 @@ export const SecretRotationItem = ({
<img
src={`/images/integrations/${image}`}
style={{
height: "11px",
width: "11px"
}}
alt={`${rotationType} logo`}