From 0366506213df7972c415466ad790bbcdc273dd40 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 10 Dec 2024 03:30:19 +0400 Subject: [PATCH 1/4] feat(azure-app-integration): label & reference support --- backend/src/lib/api-docs/constants.ts | 2 + .../integration-sync-secret.ts | 22 +++- .../integration/integration-schema.ts | 2 + .../src/hooks/api/integrations/queries.tsx | 1 + .../azure-app-configuration/create.tsx | 114 +++++++++++++----- 5 files changed, 109 insertions(+), 32 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index fabcae408..70e3d0ccd 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1126,6 +1126,8 @@ export const INTEGRATION = { shouldAutoRedeploy: "Used by Render to trigger auto deploy.", secretGCPLabel: "The label for GCP secrets.", secretAWSTag: "The tags for AWS secrets.", + azureUseLabels: + "If enabled, each secret will be given a label that represents which Infisical environment they belong to.", githubVisibility: "Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.", githubVisibilityRepoIds: diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index c147150f0..7a1b104ce 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -299,6 +299,11 @@ const syncSecretsAzureAppConfig = async ({ value: string; } + // Format: {\"uri\":\"https://SOME-KEY-VAULT.vault.azure.net/secrets/SOME-SECRET-KEY\"} + // Also works without the backslash escapes + const azureSecretReferenceUriRegex = + /^\{(\\"|")uri(\\"|"):(\\"|")https:\/\/[a-zA-Z0-9-]+\.vault\.azure\.net\/secrets\/[a-zA-Z0-9-]+\\?\2\}$/; + const getCompleteAzureAppConfigValues = async (url: string) => { let result: AzureAppConfigKeyValue[] = []; while (url) { @@ -405,14 +410,24 @@ const syncSecretsAzureAppConfig = async ({ } // create or update secrets on Azure App Config + for await (const key of Object.keys(secrets)) { if (!(key in azureAppConfigSecrets) || secrets[key]?.value !== azureAppConfigSecrets[key]) { await request.put( `${integration.app}/kv/${key}?api-version=2023-11-01`, { - value: secrets[key]?.value + value: secrets[key]?.value, + ...(azureSecretReferenceUriRegex.test(secrets[key]?.value || "") && { + content_type: "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8" + }) }, { + ...(metadata.azureUseLabels && { + params: { + label: integration.environment.slug + } + }), + headers: { Authorization: `Bearer ${accessToken}` }, @@ -432,6 +447,11 @@ const syncSecretsAzureAppConfig = async ({ headers: { Authorization: `Bearer ${accessToken}` }, + ...(metadata.azureUseLabels && { + params: { + label: integration.environment.slug + } + }), // we force IPV4 because docker setup fails with ipv6 httpsAgent: new https.Agent({ family: 4 diff --git a/backend/src/services/integration/integration-schema.ts b/backend/src/services/integration/integration-schema.ts index d047a0c11..01a928db0 100644 --- a/backend/src/services/integration/integration-schema.ts +++ b/backend/src/services/integration/integration-schema.ts @@ -35,6 +35,8 @@ export const IntegrationMetadataSchema = z.object({ .optional() .describe(INTEGRATION.CREATE.metadata.secretAWSTag), + azureUseLabels: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.azureUseLabels), + githubVisibility: z .union([z.literal("selected"), z.literal("private"), z.literal("all")]) .optional() diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 11d42631a..a40ef35eb 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -80,6 +80,7 @@ export const useCreateIntegration = () => { key: string; value: string; }[]; + azureUseLabels?: boolean; githubVisibility?: string; githubVisibilityRepoIds?: string[]; kmsKeyId?: string; diff --git a/frontend/src/pages/integrations/azure-app-configuration/create.tsx b/frontend/src/pages/integrations/azure-app-configuration/create.tsx index c9fe4d1db..a7b137adf 100644 --- a/frontend/src/pages/integrations/azure-app-configuration/create.tsx +++ b/frontend/src/pages/integrations/azure-app-configuration/create.tsx @@ -4,7 +4,11 @@ import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; +import { + faArrowUpRightFromSquare, + faBookOpen, + faQuestionCircle +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import queryString from "query-string"; @@ -21,7 +25,9 @@ import { FormControl, Input, Select, - SelectItem + SelectItem, + Switch, + Tooltip } from "../../../components/v2"; import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; @@ -39,7 +45,8 @@ const schema = z.object({ secretPath: z.string().trim().min(1, { message: "Secret path is required" }), sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }), initialSyncBehavior: z.nativeEnum(IntegrationSyncBehavior), - secretPrefix: z.string().default("") + secretPrefix: z.string().default(""), + useLabels: z.boolean().default(false) }); type TFormSchema = z.infer; @@ -60,6 +67,7 @@ export default function AzureAppConfigurationCreateIntegration() { const router = useRouter(); const { control, + watch, setValue, handleSubmit, formState: { isSubmitting } @@ -85,8 +93,11 @@ export default function AzureAppConfigurationCreateIntegration() { } }, [workspace]); + const sourceEnv = watch("sourceEnvironment"); + const handleIntegrationSubmit = async ({ secretPath, + useLabels, sourceEnvironment, baseUrl, initialSyncBehavior, @@ -103,7 +114,8 @@ export default function AzureAppConfigurationCreateIntegration() { secretPath, metadata: { initialSyncBehavior, - secretPrefix + secretPrefix, + azureUseLabels: useLabels } }); @@ -155,35 +167,75 @@ export default function AzureAppConfigurationCreateIntegration() {
- ( - - + + )} + /> + + ( + onChange(isChecked)} + isChecked={value} + > +
+ Use Environment Labels + +

+ Use the environment slug as the label on the secret keys created in + Azure App Configuration. +
+
+ {sourceEnv && ( +

+ You have selected the{" "} + {sourceEnv} environment, + therefore the label will be set to{" "} + {sourceEnv}. +

+ )} +

+
+ } > - {sourceEnvironment.name} - - ))} - - - )} - /> + + +
+ + )} + /> + Date: Wed, 11 Dec 2024 21:17:01 +0400 Subject: [PATCH 2/4] fix: requested changes --- backend/src/lib/api-docs/constants.ts | 3 +- .../integration-sync-secret.ts | 8 +- .../integration/integration-schema.ts | 2 +- .../src/hooks/api/integrations/queries.tsx | 2 +- frontend/src/hooks/api/integrations/types.ts | 1 + .../azure-app-configuration/create.tsx | 91 +++++++++++-------- .../components/IntegrationSettingsSection.tsx | 3 +- 7 files changed, 61 insertions(+), 49 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 70e3d0ccd..5326a977c 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1126,8 +1126,7 @@ export const INTEGRATION = { shouldAutoRedeploy: "Used by Render to trigger auto deploy.", secretGCPLabel: "The label for GCP secrets.", secretAWSTag: "The tags for AWS secrets.", - azureUseLabels: - "If enabled, each secret will be given a label that represents which Infisical environment they belong to.", + azureLabel: "Define which label to assign to secrets created in Azure App Configuration.", githubVisibility: "Define where the secrets from the Github Integration should be visible. Option 'selected' lets you directly define which repositories to sync secrets to.", githubVisibilityRepoIds: diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 7a1b104ce..53ff9ca67 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -422,9 +422,9 @@ const syncSecretsAzureAppConfig = async ({ }) }, { - ...(metadata.azureUseLabels && { + ...(metadata.azureLabel && { params: { - label: integration.environment.slug + label: metadata.azureLabel } }), @@ -447,9 +447,9 @@ const syncSecretsAzureAppConfig = async ({ headers: { Authorization: `Bearer ${accessToken}` }, - ...(metadata.azureUseLabels && { + ...(metadata.azureLabel && { params: { - label: integration.environment.slug + label: metadata.azureLabel } }), // we force IPV4 because docker setup fails with ipv6 diff --git a/backend/src/services/integration/integration-schema.ts b/backend/src/services/integration/integration-schema.ts index 01a928db0..de4790188 100644 --- a/backend/src/services/integration/integration-schema.ts +++ b/backend/src/services/integration/integration-schema.ts @@ -35,7 +35,7 @@ export const IntegrationMetadataSchema = z.object({ .optional() .describe(INTEGRATION.CREATE.metadata.secretAWSTag), - azureUseLabels: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.azureUseLabels), + azureLabel: z.string().optional().describe(INTEGRATION.CREATE.metadata.azureLabel), githubVisibility: z .union([z.literal("selected"), z.literal("private"), z.literal("all")]) diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index a40ef35eb..5c059ae98 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -80,7 +80,7 @@ export const useCreateIntegration = () => { key: string; value: string; }[]; - azureUseLabels?: boolean; + azureLabel?: string; githubVisibility?: string; githubVisibilityRepoIds?: string[]; kmsKeyId?: string; diff --git a/frontend/src/hooks/api/integrations/types.ts b/frontend/src/hooks/api/integrations/types.ts index 0346b065a..7054befc7 100644 --- a/frontend/src/hooks/api/integrations/types.ts +++ b/frontend/src/hooks/api/integrations/types.ts @@ -41,6 +41,7 @@ export type TIntegration = { key: string; value: string; }[]; + azureLabel?: string; kmsKeyId?: string; secretSuffix?: string; diff --git a/frontend/src/pages/integrations/azure-app-configuration/create.tsx b/frontend/src/pages/integrations/azure-app-configuration/create.tsx index a7b137adf..13c233f53 100644 --- a/frontend/src/pages/integrations/azure-app-configuration/create.tsx +++ b/frontend/src/pages/integrations/azure-app-configuration/create.tsx @@ -14,6 +14,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import queryString from "query-string"; import { z } from "zod"; +import { createNotification } from "@app/components/notifications"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { useCreateIntegration } from "@app/hooks/api"; import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types"; @@ -46,7 +47,8 @@ const schema = z.object({ sourceEnvironment: z.string().trim().min(1, { message: "Source environment is required" }), initialSyncBehavior: z.nativeEnum(IntegrationSyncBehavior), secretPrefix: z.string().default(""), - useLabels: z.boolean().default(false) + useLabels: z.boolean().default(false), + azureLabel: z.string().min(1).optional() }); type TFormSchema = z.infer; @@ -93,7 +95,7 @@ export default function AzureAppConfigurationCreateIntegration() { } }, [workspace]); - const sourceEnv = watch("sourceEnvironment"); + const shouldUseLabels = watch("useLabels"); const handleIntegrationSubmit = async ({ secretPath, @@ -101,11 +103,20 @@ export default function AzureAppConfigurationCreateIntegration() { sourceEnvironment, baseUrl, initialSyncBehavior, - secretPrefix + secretPrefix, + azureLabel }: TFormSchema) => { try { if (!integrationAuth?.id) return; + if (useLabels && !azureLabel) { + createNotification({ + type: "error", + text: "Label must be provided when 'Use Labels' is enabled" + }); + return; + } + await mutateAsync({ integrationAuthId: integrationAuth?.id, isActive: true, @@ -115,7 +126,7 @@ export default function AzureAppConfigurationCreateIntegration() { metadata: { initialSyncBehavior, secretPrefix, - azureUseLabels: useLabels + ...(useLabels && { azureLabel }) } }); @@ -167,7 +178,7 @@ export default function AzureAppConfigurationCreateIntegration() {
-
+
- ( - onChange(isChecked)} - isChecked={value} - > -
- Use Environment Labels - -

- Use the environment slug as the label on the secret keys created in - Azure App Configuration. -
-
- {sourceEnv && ( -

- You have selected the{" "} - {sourceEnv} environment, - therefore the label will be set to{" "} - {sourceEnv}. -

- )} -

-
- } +
+ ( + onChange(isChecked)} + isChecked={value} + > +
+ Use Labels + + + +
+
+ )} + /> + + {shouldUseLabels && ( + ( + - - -
-
+ + + )} + /> )} - /> +
{ Object.entries(integration.metadata).map(([key, value]) => (

- {metadataMappings[key as keyof typeof metadataMappings]} + {!!value && metadataMappings[key as keyof typeof metadataMappings]}

{renderValue(key as MetadataKey, value)}

From 111605a9453042a2d4dc79559ac34e7eb08dca0c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 11 Dec 2024 21:20:16 +0400 Subject: [PATCH 3/4] fix: ui improvement --- .../azure-app-configuration/create.tsx | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/frontend/src/pages/integrations/azure-app-configuration/create.tsx b/frontend/src/pages/integrations/azure-app-configuration/create.tsx index 13c233f53..9b647c2a2 100644 --- a/frontend/src/pages/integrations/azure-app-configuration/create.tsx +++ b/frontend/src/pages/integrations/azure-app-configuration/create.tsx @@ -4,11 +4,7 @@ import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { - faArrowUpRightFromSquare, - faBookOpen, - faQuestionCircle -} from "@fortawesome/free-solid-svg-icons"; +import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import queryString from "query-string"; @@ -24,11 +20,11 @@ import { Card, CardTitle, FormControl, + FormLabel, Input, Select, SelectItem, - Switch, - Tooltip + Switch } from "../../../components/v2"; import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; @@ -209,7 +205,7 @@ export default function AzureAppConfigurationCreateIntegration() { )} /> -
+
onChange(isChecked)} isChecked={value} > -
- Use Labels - - - -
+ )} /> From 68a3291235247d44dded81edcb7b9024358b8e10 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 16 Dec 2024 23:24:08 +0100 Subject: [PATCH 4/4] misc: requested changes --- .../integration-sync-secret-fns.ts | 35 +++++++++++++++++++ .../integration-sync-secret.ts | 19 +++++----- 2 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 backend/src/services/integration-auth/integration-sync-secret-fns.ts diff --git a/backend/src/services/integration-auth/integration-sync-secret-fns.ts b/backend/src/services/integration-auth/integration-sync-secret-fns.ts new file mode 100644 index 000000000..df8b990af --- /dev/null +++ b/backend/src/services/integration-auth/integration-sync-secret-fns.ts @@ -0,0 +1,35 @@ +export const isAzureKeyVaultReference = (uri: string) => { + const tryJsonDecode = () => { + try { + return (JSON.parse(uri) as { uri: string }).uri || uri; + } catch { + return uri; + } + }; + + const cleanUri = tryJsonDecode(); + + if (!cleanUri.startsWith("https://")) { + return false; + } + + if (!cleanUri.includes(".vault.azure.net/secrets/")) { + return false; + } + + // 3. Check for non-empty string between https:// and .vault.azure.net/secrets/ + const parts = cleanUri.split(".vault.azure.net/secrets/"); + const vaultName = parts[0].replace("https://", ""); + if (!vaultName) { + return false; + } + + // 4. Check for non-empty secret name + const secretParts = parts[1].split("/"); + const secretName = secretParts[0]; + if (!secretName) { + return false; + } + + return true; +}; diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 53ff9ca67..d9c1ac3e6 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -46,6 +46,7 @@ import { Integrations, IntegrationUrls } from "./integration-list"; +import { isAzureKeyVaultReference } from "./integration-sync-secret-fns"; const getSecretKeyValuePair = (secrets: Record) => Object.keys(secrets).reduce>((prev, key) => { @@ -299,11 +300,6 @@ const syncSecretsAzureAppConfig = async ({ value: string; } - // Format: {\"uri\":\"https://SOME-KEY-VAULT.vault.azure.net/secrets/SOME-SECRET-KEY\"} - // Also works without the backslash escapes - const azureSecretReferenceUriRegex = - /^\{(\\"|")uri(\\"|"):(\\"|")https:\/\/[a-zA-Z0-9-]+\.vault\.azure\.net\/secrets\/[a-zA-Z0-9-]+\\?\2\}$/; - const getCompleteAzureAppConfigValues = async (url: string) => { let result: AzureAppConfigKeyValue[] = []; while (url) { @@ -325,11 +321,12 @@ const syncSecretsAzureAppConfig = async ({ }; const metadata = IntegrationMetadataSchema.parse(integration.metadata); - const azureAppConfigSecrets = ( - await getCompleteAzureAppConfigValues( - `${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix || ""}*` - ) - ).reduce( + + const azureAppConfigValuesUrl = `${integration.app}/kv?api-version=2023-11-01&key=${metadata.secretPrefix}*${ + metadata.azureLabel ? `&label=${metadata.azureLabel}` : "" + }`; + + const azureAppConfigSecrets = (await getCompleteAzureAppConfigValues(azureAppConfigValuesUrl)).reduce( (accum, entry) => { accum[entry.key] = entry.value; @@ -417,7 +414,7 @@ const syncSecretsAzureAppConfig = async ({ `${integration.app}/kv/${key}?api-version=2023-11-01`, { value: secrets[key]?.value, - ...(azureSecretReferenceUriRegex.test(secrets[key]?.value || "") && { + ...(isAzureKeyVaultReference(secrets[key]?.value || "") && { content_type: "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8" }) },