diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts index 7fee08164..3dcdac30b 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts @@ -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 }; diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/ldap-password-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/ldap-password-rotation-router.ts new file mode 100644 index 000000000..04d2b50ac --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/ldap-password-rotation-router.ts @@ -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 + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts index 2383d98e2..772f70035 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts @@ -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 ]); diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/index.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/index.ts new file mode 100644 index 000000000..55929169a --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/index.ts @@ -0,0 +1,3 @@ +export * from "./ldap-password-rotation-constants"; +export * from "./ldap-password-rotation-schemas"; +export * from "./ldap-password-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-constants.ts new file mode 100644 index 000000000..061bf11ea --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-constants.ts @@ -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" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts new file mode 100644 index 000000000..0fd01b753 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts @@ -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) => { + 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 = async ( + callback + ) => { + const credentials = await $rotatePassword(); + + return callback(credentials); + }; + + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + _, + callback + ) => { + // we just rotate to a new password, essentially revoking old credentials + await $rotatePassword(); + + return callback(); + }; + + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + _, + callback + ) => { + const credentials = await $rotatePassword(); + + return callback(credentials); + }; + + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ( + generatedCredentials + ) => { + const secrets = [ + { + key: secretsMapping.dn, + value: generatedCredentials.dn + }, + { + key: secretsMapping.password, + value: generatedCredentials.password + } + ]; + + return secrets; + }; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-schemas.ts new file mode 100644 index 000000000..e99569d9a --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-schemas.ts @@ -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 +}); diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types.ts new file mode 100644 index 000000000..cb15b0734 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-types.ts @@ -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; + +export type TLdapPasswordRotationInput = z.infer; + +export type TLdapPasswordRotationListItem = z.infer; + +export type TLdapPasswordRotationWithConnection = TLdapPasswordRotation & { + connection: TLdapConnection; +}; + +export type TLdapPasswordRotationGeneratedCredentials = z.infer; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts index 1e387a3b9..4ddf4ee0c 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts @@ -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" } diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index 87115e7f4..23452d7d4 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -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.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>>; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts index af46cb36c..4d51a23c3 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema.ts @@ -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 ]); diff --git a/backend/src/ee/services/secret-rotation-v2/shared/general/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/general/index.ts new file mode 100644 index 000000000..9b2148414 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/general/index.ts @@ -0,0 +1 @@ +export * from "./password-requirements-schema"; diff --git a/backend/src/ee/services/secret-rotation-v2/shared/general/password-requirements-schema.ts b/backend/src/ee/services/secret-rotation-v2/shared/general/password-requirements-schema.ts new file mode 100644 index 000000000..5d575239d --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/shared/general/password-requirements-schema.ts @@ -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); diff --git a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts index dfe4c22ed..9b2eb7839 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/utils/index.ts @@ -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", diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b5181ae81..18157d979 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -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." diff --git a/backend/src/lib/regex/index.ts b/backend/src/lib/regex/index.ts new file mode 100644 index 000000000..68ba7671d --- /dev/null +++ b/backend/src/lib/regex/index.ts @@ -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]+=[^,+="<>#;\\\\]+)*))*)?$/; diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index d472a9cb1..0b565f7d8 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -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 ]); diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index de9db9d85..c2398fd78 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -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 { + registerAppConnectionEndpoints({ + app: AppConnection.LDAP, + server, + sanitizedResponseSchema: SanitizedLdapConnectionSchema, + createSchema: CreateLdapConnectionSchema, + updateSchema: UpdateLdapConnectionSchema + }); +}; diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 8852ed6bc..de20f3f64 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -13,6 +13,7 @@ export enum AppConnection { Camunda = "camunda", Windmill = "windmill", Auth0 = "auth0", + LDAP = "ldap", TeamCity = "teamcity" } diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 076f52f99..92595619c 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -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 }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 41cef4af1..524993b23 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -15,5 +15,6 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Camunda]: "Camunda", [AppConnection.Windmill]: "Windmill", [AppConnection.Auth0]: "Auth0", + [AppConnection.LDAP]: "LDAP", [AppConnection.TeamCity]: "TeamCity" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 5ca0c1424..e872c07e4 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -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 { + 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((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(); + } +}; diff --git a/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts new file mode 100644 index 000000000..91884b914 --- /dev/null +++ b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts @@ -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() +}); diff --git a/backend/src/services/app-connection/ldap/ldap-connection-types.ts b/backend/src/services/app-connection/ldap/ldap-connection-types.ts new file mode 100644 index 000000000..ee69b2542 --- /dev/null +++ b/backend/src/services/app-connection/ldap/ldap-connection-types.ts @@ -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; + +export type TLdapConnectionInput = z.infer & { + app: AppConnection.LDAP; +}; + +export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema; + +export type TLdapConnectionConfig = DiscriminativePick & { + orgId: string; +}; diff --git a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts index 38ef0eef6..994f9a40d 100644 --- a/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts +++ b/backend/src/services/app-connection/mssql/mssql-connection-schemas.ts @@ -31,7 +31,8 @@ export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [ port: true, username: true, sslEnabled: true, - sslRejectUnauthorized: true + sslRejectUnauthorized: true, + sslCertificate: true }) }) ]); diff --git a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts index 510f7b7d0..1ddf1e2da 100644 --- a/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts +++ b/backend/src/services/app-connection/postgres/postgres-connection-schemas.ts @@ -29,7 +29,8 @@ export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method", port: true, username: true, sslEnabled: true, - sslRejectUnauthorized: true + sslRejectUnauthorized: true, + sslCertificate: true }) }) ]); diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index e576d6768..bc9c4afa3 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -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; diff --git a/docs/api-reference/endpoints/app-connections/ldap/available.mdx b/docs/api-reference/endpoints/app-connections/ldap/available.mdx new file mode 100644 index 000000000..b42f2bc3d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/ldap/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/create.mdx b/docs/api-reference/endpoints/app-connections/ldap/create.mdx new file mode 100644 index 000000000..181a76902 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/ldap" +--- + + + Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/app-connections/ldap/delete.mdx b/docs/api-reference/endpoints/app-connections/ldap/delete.mdx new file mode 100644 index 000000000..4888fd04d --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/ldap/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/ldap/get-by-id.mdx new file mode 100644 index 000000000..7c4524ed8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/ldap/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/ldap/get-by-name.mdx new file mode 100644 index 000000000..dc7516bba --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/ldap/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/list.mdx b/docs/api-reference/endpoints/app-connections/ldap/list.mdx new file mode 100644 index 000000000..e909c9266 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/ldap" +--- diff --git a/docs/api-reference/endpoints/app-connections/ldap/update.mdx b/docs/api-reference/endpoints/app-connections/ldap/update.mdx new file mode 100644 index 000000000..06c7f7f77 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/ldap/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/ldap/{connectionId}" +--- + + + Check out the configuration docs for [LDAP Connections](/integrations/app-connections/ldap) to learn how to obtain + the required credentials. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/create.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/create.mdx new file mode 100644 index 000000000..682b531ad --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/create.mdx @@ -0,0 +1,9 @@ +--- +title: "Create" +openapi: "POST /api/v2/secret-rotations/ldap-password" +--- + + + Check out the configuration docs for [LDAP Password Rotations](/documentation/platform/secret-rotation/ldap-password) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/delete.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/delete.mdx new file mode 100644 index 000000000..d4cee951f --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v2/secret-rotations/ldap-password/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-id.mdx new file mode 100644 index 000000000..f422d036d --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v2/secret-rotations/ldap-password/{rotationId}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-name.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-name.mdx new file mode 100644 index 000000000..68de6a722 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v2/secret-rotations/ldap-password/rotation-name/{rotationName}" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id.mdx new file mode 100644 index 000000000..6aed49218 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/get-generated-credentials-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Credentials by ID" +openapi: "GET /api/v2/secret-rotations/ldap-password/{rotationId}/generated-credentials" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/list.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/list.mdx new file mode 100644 index 000000000..bf2bb5562 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v2/secret-rotations/ldap-password" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets.mdx new file mode 100644 index 000000000..8ad2ae52b --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/rotate-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Rotate Secrets" +openapi: "POST /api/v2/secret-rotations/ldap-password/{rotationId}/rotate-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-rotations/ldap-password/update.mdx b/docs/api-reference/endpoints/secret-rotations/ldap-password/update.mdx new file mode 100644 index 000000000..b59ea5250 --- /dev/null +++ b/docs/api-reference/endpoints/secret-rotations/ldap-password/update.mdx @@ -0,0 +1,9 @@ +--- +title: "Update" +openapi: "PATCH /api/v2/secret-rotations/ldap-password/{rotationId}" +--- + + + Check out the configuration docs for [LDAP Rotations](/documentation/platform/secret-rotation/ldap-password) to learn how to obtain the + required parameters. + \ No newline at end of file diff --git a/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx b/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx index 3845a3879..0fd43f2c4 100644 --- a/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx +++ b/docs/documentation/platform/secret-rotation/auth0-client-secret.mdx @@ -1,5 +1,5 @@ --- -title: "Auth0 Client Secret" +title: "Auth0 Client Secret Rotation" description: "Learn how to automatically rotate Auth0 Client Secrets." --- diff --git a/docs/documentation/platform/secret-rotation/ldap-password.mdx b/docs/documentation/platform/secret-rotation/ldap-password.mdx new file mode 100644 index 000000000..103fe4656 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/ldap-password.mdx @@ -0,0 +1,173 @@ +--- +title: "LDAP Password Rotation" +description: "Learn how to automatically rotate LDAP passwords." +--- + + + 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. + + +## Prerequisites + +- Create an [LDAP Connection](/integrations/app-connections/ldap) with the **Secret Rotation** requirements + +## Create an LDAP Password Rotation in Infisical + + + + 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. + + LDAP Password Rotations require an LDAP Connection that uses ldaps:// protocol. + + - **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. + + 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. + + + + 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) + + + 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" + } + }' + ``` + + + 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. + + + ### 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": "-_.~!*" + } + } + } + } + ``` + + diff --git a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx index 8789e4152..c20622f26 100644 --- a/docs/documentation/platform/secret-rotation/mssql-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/mssql-credentials.mdx @@ -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." --- diff --git a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx index e0606e6ab..55175d967 100644 --- a/docs/documentation/platform/secret-rotation/postgres-credentials.mdx +++ b/docs/documentation/platform/secret-rotation/postgres-credentials.mdx @@ -1,5 +1,5 @@ --- -title: "PostgreSQL Credentials" +title: "PostgreSQL Credentials Rotation" description: "Learn how to automatically rotate PostgreSQL credentials." --- diff --git a/docs/images/app-connections/ldap/create-simple-bind-method.png b/docs/images/app-connections/ldap/create-simple-bind-method.png new file mode 100644 index 000000000..e7fff6789 Binary files /dev/null and b/docs/images/app-connections/ldap/create-simple-bind-method.png differ diff --git a/docs/images/app-connections/ldap/select-ldap-connection.png b/docs/images/app-connections/ldap/select-ldap-connection.png new file mode 100644 index 000000000..48465df67 Binary files /dev/null and b/docs/images/app-connections/ldap/select-ldap-connection.png differ diff --git a/docs/images/app-connections/ldap/simple-bind-connection.png b/docs/images/app-connections/ldap/simple-bind-connection.png new file mode 100644 index 000000000..8013a4687 Binary files /dev/null and b/docs/images/app-connections/ldap/simple-bind-connection.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png new file mode 100644 index 000000000..91a2f2fb3 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-configuration.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png new file mode 100644 index 000000000..1725c4355 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-confirm.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-created.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-created.png new file mode 100644 index 000000000..4172ec7f7 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-created.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-details.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-details.png new file mode 100644 index 000000000..ed41c13ad Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-details.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png new file mode 100644 index 000000000..dfe723b06 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-parameters.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png b/docs/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png new file mode 100644 index 000000000..997073bc5 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/ldap-password-secrets-mapping.png differ diff --git a/docs/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png b/docs/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png new file mode 100644 index 000000000..4dd500fe1 Binary files /dev/null and b/docs/images/secret-rotations-v2/ldap-password/select-ldap-password-option.png differ diff --git a/docs/integrations/app-connections/ldap.mdx b/docs/integrations/app-connections/ldap.mdx new file mode 100644 index 000000000..63c4bfed1 --- /dev/null +++ b/docs/integrations/app-connections/ldap.mdx @@ -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: + + + + + 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 + + + + + +## Setup LDAP Connection in Infisical + + + + 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) + + + 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": "", + "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": "..." + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index cebdf9e7f..47a4c0a76 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -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": [ diff --git a/frontend/public/images/integrations/LDAP.png b/frontend/public/images/integrations/LDAP.png new file mode 100644 index 000000000..4cf290176 Binary files /dev/null and b/frontend/public/images/integrations/LDAP.png differ diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx index d2340f42d..420889d2b 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewAuth0ClientSecretRotationGeneratedCredentials.tsx @@ -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,40 +15,23 @@ export const ViewAuth0ClientSecretRotationGeneratedCredentials = ({ const inactiveCredentials = generatedCredentials[inactiveIndex]; return ( - <> - - {activeCredentials?.clientId} - - {activeCredentials?.clientSecret} - - - } - inactiveCredentials={ - <> - {inactiveCredentials?.clientId} - - {inactiveCredentials?.clientSecret} - - - } - /> - -

- Due to how Auth0 client secrets are rotated, retired credentials will not be able to - authenticate with Auth0 during their{" "} - - inactive period - - . This is a limitation of the Auth0 platform and cannot be rectified by Infisical. -

-
- + + {activeCredentials?.clientId} + + {activeCredentials?.clientSecret} + + + } + inactiveCredentials={ + <> + {inactiveCredentials?.clientId} + + {inactiveCredentials?.clientSecret} + + + } + /> ); }; diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx new file mode 100644 index 000000000..238dabea3 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewLdapPasswordRotationGeneratedCredentials.tsx @@ -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 ( + + + {activeCredentials?.dn} + + + {activeCredentials?.password} + + + } + inactiveCredentials={ + <> + + {inactiveCredentials?.dn} + + + {inactiveCredentials?.password} + + + } + /> + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx index 12ad1f9a5..d5af51eef 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx @@ -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 = ( + + ); + break; case SecretRotation.AwsIamUserSecret: Component = ( { throw new Error("Unhandled View Generated Credential Rotation Type"); } + const appName = APP_CONNECTION_MAP[SECRET_ROTATION_CONNECTION_MAP[type]].name; + return (
{Component} + {!IS_ROTATION_DUAL_CREDENTIALS[type] && ( + +

+ 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{" "} + + inactive period + + . This is a limitation of {appName} and cannot be rectified by Infisical. +

+
+ )} {nextRotationAt && (
diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx new file mode 100644 index 000000000..9c9d8329f --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/LdapPasswordRotationParametersFields.tsx @@ -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 ( + <> + ( + + + + )} + /> +
+
+ Password Requirements +
+
+ ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(Number(e.target.value))} + /> + + )} + /> + ( + + field.onChange(e.target.value)} + /> + + )} + /> +
+
+ + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx index e0841d096..cdbf63111 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ParametersFields/SecretRotationV2ParametersFields.tsx @@ -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.PostgresCredentials]: SqlCredentialsRotationParametersFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationParametersFields, [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationParametersFields, + [SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields, [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields }; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/LdapPasswordRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/LdapPasswordRotationReviewFields.tsx new file mode 100644 index 000000000..1ffcad139 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/LdapPasswordRotationReviewFields.tsx @@ -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 ( + <> + + {parameters.dn} + + + {dn} + {password} + + + ); +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx index d8624e308..23ee25d7f 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx @@ -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.PostgresCredentials]: SqlCredentialsRotationReviewFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationReviewFields, [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationReviewFields, + [SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields, [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields }; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx new file mode 100644 index 000000000..01d2e0d74 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/LdapPasswordRotationSecretsMappingFields.tsx @@ -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: ( + ( + + + + )} + control={control} + name="secretsMapping.dn" + /> + ) + }, + { + name: "Password", + input: ( + ( + + + + )} + control={control} + name="secretsMapping.password" + /> + ) + } + ]; + + return ; +}; diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx index 6ede945db..16bffe6cf 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx @@ -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.PostgresCredentials]: SqlCredentialsRotationSecretsMappingFields, [SecretRotation.MsSqlCredentials]: SqlCredentialsRotationSecretsMappingFields, [SecretRotation.Auth0ClientSecret]: Auth0ClientSecretRotationSecretsMappingFields, + [SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields, [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields }; diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts index acd56e571..6dd65b0bb 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts @@ -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 ]); diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema.ts new file mode 100644 index 000000000..e18609f04 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema.ts @@ -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); diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts index 44b4c194f..284b705e4 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/index.ts @@ -1 +1,2 @@ +export * from "./password-requirements-schema"; export * from "./sql-credentials-rotation-schema"; diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/shared/password-requirements-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/password-requirements-schema.ts new file mode 100644 index 000000000..a02852ec8 --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/shared/password-requirements-schema.ts @@ -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: "-_.~!*" +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx index f9948fcaa..4f2718089 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/TeamCitySyncFields.tsx @@ -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} diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index fd8b6666d..02c0fbbcb 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -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}`); } diff --git a/frontend/src/helpers/secretRotationsV2.ts b/frontend/src/helpers/secretRotationsV2.ts index e5ab1419d..8acfaafe5 100644 --- a/frontend/src/helpers/secretRotationsV2.ts +++ b/frontend/src/helpers/secretRotationsV2.ts @@ -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.PostgresCredentials]: true, [SecretRotation.MsSqlCredentials]: true, [SecretRotation.Auth0ClientSecret]: false, + [SecretRotation.LdapPassword]: false, [SecretRotation.AwsIamUserSecret]: true }; diff --git a/frontend/src/helpers/string.ts b/frontend/src/helpers/string.ts index 109b51d49..ddd9fb7c9 100644 --- a/frontend/src/helpers/string.ts +++ b/frontend/src/helpers/string.ts @@ -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]+=[^,+="<>#;\\\\]+)*))*)?$/; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 31e2df71f..5b9d3fad4 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -13,5 +13,6 @@ export enum AppConnection { Camunda = "camunda", Windmill = "windmill", Auth0 = "auth0", + LDAP = "ldap", TeamCity = "teamcity" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 3122ca32c..10c1076cd 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -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; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 929ca796d..2eabee1f2 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -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; @@ -89,5 +92,6 @@ export type TAppConnectionMap = { [AppConnection.Camunda]: TCamundaConnection; [AppConnection.Windmill]: TWindmillConnection; [AppConnection.Auth0]: TAuth0Connection; + [AppConnection.LDAP]: TLdapConnection; [AppConnection.TeamCity]: TTeamCityConnection; }; diff --git a/frontend/src/hooks/api/appConnections/types/ldap-connection.ts b/frontend/src/hooks/api/appConnections/types/ldap-connection.ts new file mode 100644 index 000000000..95165fc5d --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/ldap-connection.ts @@ -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; + }; +}; diff --git a/frontend/src/hooks/api/secretRotationsV2/enums.ts b/frontend/src/hooks/api/secretRotationsV2/enums.ts index 1e387a3b9..4ddf4ee0c 100644 --- a/frontend/src/hooks/api/secretRotationsV2/enums.ts +++ b/frontend/src/hooks/api/secretRotationsV2/enums.ts @@ -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" } diff --git a/frontend/src/hooks/api/secretRotationsV2/types/index.ts b/frontend/src/hooks/api/secretRotationsV2/types/index.ts index 212175ae0..4119efc15 100644 --- a/frontend/src/hooks/api/secretRotationsV2/types/index.ts +++ b/frontend/src/hooks/api/secretRotationsV2/types/index.ts @@ -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; }; diff --git a/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts new file mode 100644 index 000000000..b8d2ade2b --- /dev/null +++ b/frontend/src/hooks/api/secretRotationsV2/types/ldap-password-rotation.ts @@ -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"]; + }; +}; diff --git a/frontend/src/pages/middlewares/authenticate.tsx b/frontend/src/pages/middlewares/authenticate.tsx index 2f5789096..03005ce05 100644 --- a/frontend/src/pages/middlewares/authenticate.tsx +++ b/frontend/src/pages/middlewares/authenticate.tsx @@ -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 }) => { diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 1259c50d6..f47de43dd 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -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 ; case AppConnection.Auth0: return ; + case AppConnection.LDAP: + return ; case AppConnection.TeamCity: return ; default: @@ -156,8 +159,11 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.LDAP: + return ; case AppConnection.TeamCity: return ; + default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx new file mode 100644 index 000000000..7346f84af --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx @@ -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; +}; + +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; + +export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + + const form = useForm({ + 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 ( + +
{ + setSelectedTabIndex(0); + handleSubmit(onSubmit)(e); + }} + > + {!isUpdate && } +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + + + `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 + + + `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"}) + + + {selectedTabIndex === 1 && ( +
Requires ldaps:// URL
+ )} + + + ( + + + + )} + /> +
+ ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+
+ + ( + +