From 0e6aed7497cf5f71e0dabb8e713ed2956d25b85c Mon Sep 17 00:00:00 2001 From: Meet Date: Mon, 30 Sep 2024 19:32:24 +0530 Subject: [PATCH 1/8] feat: add LDAP support for dynamic secrets --- backend/package-lock.json | 25 ++ backend/package.json | 3 + .../dynamic-secret/providers/index.ts | 4 +- .../dynamic-secret/providers/models.ts | 17 +- frontend/src/hooks/api/dynamicSecret/types.ts | 18 +- .../CreateDynamicSecretForm.tsx | 26 ++ .../CreateDynamicSecretForm/LdapInputForm.tsx | 283 ++++++++++++++++++ .../CreateDynamicSecretLease.tsx | 25 ++ .../EditDynamicSecretForm.tsx | 19 ++ .../EditDynamicSecretLdapForm.tsx | 268 +++++++++++++++++ 10 files changed, 683 insertions(+), 5 deletions(-) create mode 100644 frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx create mode 100644 frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretLdapForm.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json index 6ea34f692..af9d87785 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -61,10 +61,12 @@ "jwks-rsa": "^3.1.0", "knex": "^3.0.1", "ldapjs": "^3.0.7", + "ldif": "^0.5.1", "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", "mongodb": "^6.8.1", "ms": "^2.1.3", + "mustache": "^4.2.0", "mysql2": "^3.9.8", "nanoid": "^3.3.4", "nodemailer": "^6.9.9", @@ -108,6 +110,7 @@ "@types/jsrp": "^0.2.6", "@types/libsodium-wrappers": "^0.7.13", "@types/lodash.isequal": "^4.5.8", + "@types/mustache": "^4.2.5", "@types/node": "^20.9.5", "@types/nodemailer": "^6.4.14", "@types/passport-github": "^1.1.12", @@ -7074,6 +7077,13 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, + "node_modules/@types/mustache": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", + "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.9.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.5.tgz", @@ -13008,6 +13018,12 @@ "verror": "^1.10.1" } }, + "node_modules/ldif": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/ldif/-/ldif-0.5.1.tgz", + "integrity": "sha512-8s46m/r2lSFO2+DqMxqWiJ10iiL4tuR5LC/KndV+E5//OAOzOx5s3HS5O34PJ5+kyaCA+K2oCaEPaDRfXUnQow==", + "license": "MIT" + }, "node_modules/leven": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", @@ -13704,6 +13720,15 @@ "node": ">= 6" } }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mylas": { "version": "2.1.13", "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz", diff --git a/backend/package.json b/backend/package.json index 21f841da7..b3ac08249 100644 --- a/backend/package.json +++ b/backend/package.json @@ -71,6 +71,7 @@ "@types/jsrp": "^0.2.6", "@types/libsodium-wrappers": "^0.7.13", "@types/lodash.isequal": "^4.5.8", + "@types/mustache": "^4.2.5", "@types/node": "^20.9.5", "@types/nodemailer": "^6.4.14", "@types/passport-github": "^1.1.12", @@ -158,10 +159,12 @@ "jwks-rsa": "^3.1.0", "knex": "^3.0.1", "ldapjs": "^3.0.7", + "ldif": "^0.5.1", "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", "mongodb": "^6.8.1", "ms": "^2.1.3", + "mustache": "^4.2.0", "mysql2": "^3.9.8", "nanoid": "^3.3.4", "nodemailer": "^6.9.9", diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index 8f2dbdc13..b3c243b5e 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -3,6 +3,7 @@ import { AwsIamProvider } from "./aws-iam"; import { AzureEntraIDProvider } from "./azure-entra-id"; import { CassandraProvider } from "./cassandra"; import { ElasticSearchProvider } from "./elastic-search"; +import { LdapProvider } from "./ldap"; import { DynamicSecretProviders } from "./models"; import { MongoAtlasProvider } from "./mongo-atlas"; import { MongoDBProvider } from "./mongo-db"; @@ -20,5 +21,6 @@ export const buildDynamicSecretProviders = () => ({ [DynamicSecretProviders.MongoDB]: MongoDBProvider(), [DynamicSecretProviders.ElasticSearch]: ElasticSearchProvider(), [DynamicSecretProviders.RabbitMq]: RabbitMqProvider(), - [DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider() + [DynamicSecretProviders.AzureEntraID]: AzureEntraIDProvider(), + [DynamicSecretProviders.Ldap]: LdapProvider() }); diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index 18a7b3dc9..2f6f82c04 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -174,6 +174,17 @@ export const AzureEntraIDSchema = z.object({ clientSecret: z.string().trim().min(1) }); +export const LdapSchema = z.object({ + url: z.string().trim().min(1), + binddn: z.string().trim().min(1), + bindpass: z.string().trim().min(1), + ca: z.string().optional(), + + creationLdif: z.string().trim().min(1), + revocationLdif: z.string().trim().min(1), + rollbackLdif: z.string().trim().min(1) +}); + export enum DynamicSecretProviders { SqlDatabase = "sql-database", Cassandra = "cassandra", @@ -184,7 +195,8 @@ export enum DynamicSecretProviders { ElasticSearch = "elastic-search", MongoDB = "mongo-db", RabbitMq = "rabbit-mq", - AzureEntraID = "azure-entra-id" + AzureEntraID = "azure-entra-id", + Ldap = "ldap" } export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ @@ -197,7 +209,8 @@ export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal(DynamicSecretProviders.ElasticSearch), inputs: DynamicSecretElasticSearchSchema }), z.object({ type: z.literal(DynamicSecretProviders.MongoDB), inputs: DynamicSecretMongoDBSchema }), z.object({ type: z.literal(DynamicSecretProviders.RabbitMq), inputs: DynamicSecretRabbitMqSchema }), - z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }) + z.object({ type: z.literal(DynamicSecretProviders.AzureEntraID), inputs: AzureEntraIDSchema }), + z.object({ type: z.literal(DynamicSecretProviders.Ldap), inputs: LdapSchema }) ]); export type TDynamicProviderFns = { diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index e97234bc4..f4bd05bfa 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -25,7 +25,8 @@ export enum DynamicSecretProviders { ElasticSearch = "elastic-search", MongoDB = "mongo-db", RabbitMq = "rabbit-mq", - AzureEntraId = "azure-entra-id" + AzureEntraId = "azure-entra-id", + Ldap = "ldap" } export enum SqlProviders { @@ -188,7 +189,20 @@ export type TDynamicSecretProvider = applicationId: string; clientSecret: string; }; - }; + } + | { + type: DynamicSecretProviders.Ldap; + inputs: { + url: string; + binddn: string; + bindpass: string; + ca?: string | undefined; + creationLdif: string; + revocationLdif: string; + rollbackLdif: string; + }; + }; + ; export type TCreateDynamicSecretDTO = { projectSlug: string; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx index a28544a33..3a92fadbf 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -14,6 +14,7 @@ import { AwsIamInputForm } from "./AwsIamInputForm"; import { AzureEntraIdInputForm } from "./AzureEntraIdInputForm"; import { CassandraInputForm } from "./CassandraInputForm"; import { ElasticSearchInputForm } from "./ElasticSearchInputForm"; +import { LdapInputForm } from "./LdapInputForm"; import { MongoAtlasInputForm } from "./MongoAtlasInputForm"; import { MongoDBDatabaseInputForm } from "./MongoDBInputForm"; import { RabbitMqInputForm } from "./RabbitMqInputForm"; @@ -83,6 +84,11 @@ const DYNAMIC_SECRET_LIST = [ icon: , provider: DynamicSecretProviders.AzureEntraId, title: "Azure Entra ID", + }, + { + icon: , + provider: DynamicSecretProviders.Ldap, + title: "LDAP", } ]; @@ -325,6 +331,26 @@ export const CreateDynamicSecretForm = ({ ) } + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.Ldap && ( + + + + ) + } + diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx new file mode 100644 index 000000000..818fee386 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/LdapInputForm.tsx @@ -0,0 +1,283 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input, TextArea } from "@app/components/v2"; +import { useCreateDynamicSecret } from "@app/hooks/api"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + provider: z.object({ + url: z.string().trim().min(1), + binddn: z.string().trim().min(1), + bindpass: z.string().trim().min(1), + ca: z.string().optional(), + + creationLdif: z.string().trim().min(1), + revocationLdif: z.string().trim().min(1), + rollbackLdif: z.string().trim().min(1), + }), + + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase") +}); + +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectSlug: string; + environment: string; +}; + +export const LdapInputForm = ( + { + onCompleted, + onCancel, + secretPath, + projectSlug, + environment, + }: Props +) => { + const { + control, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + provider: { + url: "", + binddn: "", + bindpass: "", + ca: "", + creationLdif: "", + revocationLdif: "", + rollbackLdif: "" + }, + } + }); + + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isLoading) return; + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.Ldap, inputs: provider }, + maxTTL, + name, + path: secretPath, + defaultTTL, + projectSlug, + environmentSlug: environment + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + return ( +
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
+ Configuration +
+
+
+
+ ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + +