feat(secret-syncs): azure app config & key vault support

This commit is contained in:
Daniel Hougaard
2025-02-08 04:27:17 +04:00
parent 4e41e84491
commit 5cd6a66989
70 changed files with 2065 additions and 204 deletions
@@ -1,3 +1,4 @@
import { useMemo } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -8,22 +9,28 @@ import { OrgPermissionSubjects, useOrgPermission } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { SECRET_SYNC_CONNECTION_MAP } from "@app/helpers/secretSyncs";
import { useListAvailableAppConnections } from "@app/hooks/api/appConnections";
import {
TAvailableAppConnection,
useListAvailableAppConnections
} from "@app/hooks/api/appConnections";
import { TSecretSyncForm } from "./schemas";
type Props = {
onChange?: VoidFunction;
filterConnections?: (
connections?: TAvailableAppConnection[]
) => TAvailableAppConnection[] | undefined;
};
export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
export const SecretSyncConnectionField = ({ onChange: callback, filterConnections }: Props) => {
const { permission } = useOrgPermission();
const { control, watch } = useFormContext<TSecretSyncForm>();
const destination = watch("destination");
const app = SECRET_SYNC_CONNECTION_MAP[destination];
const { data: options, isLoading } = useListAvailableAppConnections(app);
const { data: allConnections, isLoading } = useListAvailableAppConnections(app);
const connectionName = APP_CONNECTION_MAP[app].name;
@@ -32,6 +39,10 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
OrgPermissionSubjects.AppConnections
);
const availableConnections = useMemo(() => {
return filterConnections ? filterConnections(allConnections) : allConnections;
}, [allConnections]);
const appName = APP_CONNECTION_MAP[SECRET_SYNC_CONNECTION_MAP[destination]].name;
return (
@@ -55,7 +66,7 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
if (callback) callback();
}}
isLoading={isLoading}
options={options}
options={availableConnections}
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
@@ -65,7 +76,7 @@ export const SecretSyncConnectionField = ({ onChange: callback }: Props) => {
control={control}
name="connection"
/>
{options?.length === 0 && (
{availableConnections?.length === 0 && (
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
<FontAwesomeIcon className="mr-1" size="xs" icon={faInfoCircle} />
{canCreateConnection ? (
@@ -0,0 +1,64 @@
import { Controller, useFormContext } from "react-hook-form";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FormControl, Input } from "@app/components/v2";
import { AzureResources } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const AzureAppConfigurationSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AzureAppConfiguration }
>();
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.configurationUrl", "");
}}
filterConnections={(connections) => {
if (!connections) return connections;
return connections.filter(
(connection) =>
connection.app === AppConnection.Azure &&
connection.azureResource === AzureResources.AppConfiguration
);
}}
/>
<Controller
name="destinationConfig.configurationUrl"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vault Base URL"
tooltipText="Enter your Azure App Configuration URL. This is the base URL for your Azure App Configuration, e.g. https://resource-name-here.azconfig.io."
>
<Input {...field} placeholder="https://resource-name-here.azconfig.io" />
</FormControl>
)}
/>
<Controller
name="destinationConfig.label"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Label"
isOptional
tooltipText="Enter the label for the secret in Azure App Configuration."
>
<Input {...field} placeholder="infisical-secret" />
</FormControl>
)}
/>
</>
);
};
@@ -0,0 +1,48 @@
import { Controller, useFormContext } from "react-hook-form";
import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField";
import { FormControl, Input } from "@app/components/v2";
import { AzureResources } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
export const AzureKeyVaultSyncFields = () => {
const { control, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AzureKeyVault }
>();
return (
<>
<SecretSyncConnectionField
onChange={() => {
setValue("destinationConfig.vaultBaseUrl", "");
}}
filterConnections={(connections) => {
if (!connections) return connections;
return connections.filter(
(connection) =>
connection.app === AppConnection.Azure &&
connection.azureResource === AzureResources.KeyVault
);
}}
/>
<Controller
name="destinationConfig.vaultBaseUrl"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Vault Base URL"
tooltipText="Enter your Azure Key Vault URL. This is the base URL for your Azure Key Vault, e.g. https://example.vault.azure.net."
>
<Input {...field} placeholder="https://example.vault.azure.net" />
</FormControl>
)}
/>
</>
);
};
@@ -5,6 +5,8 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas";
import { AwsParameterStoreSyncFields } from "./AwsParameterStoreSyncFields";
import { AwsSecretsManagerSyncFields } from "./AwsSecretsManagerSyncFields";
import { AzureAppConfigurationSyncFields } from "./AzureAppConfigurationSyncFields";
import { AzureKeyVaultSyncFields } from "./AzureKeyVaultSyncFields";
import { GcpSyncFields } from "./GcpSyncFields";
import { GitHubSyncFields } from "./GitHubSyncFields";
@@ -22,6 +24,10 @@ export const SecretSyncDestinationFields = () => {
return <GitHubSyncFields />;
case SecretSync.GCPSecretManager:
return <GcpSyncFields />;
case SecretSync.AzureKeyVault:
return <AzureKeyVaultSyncFields />;
case SecretSync.AzureAppConfiguration:
return <AzureAppConfigurationSyncFields />;
default:
throw new Error(`Unhandled Destination Config Field: ${destination}`);
}
@@ -0,0 +1,20 @@
import { useFormContext } from "react-hook-form";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const AzureAppConfigurationSyncReviewFields = () => {
const { watch } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AzureAppConfiguration }
>();
const vaultBaseUrl = watch("destinationConfig.configurationUrl");
const label = watch("destinationConfig.label");
return (
<>
<SecretSyncLabel label="Configuration URL">{vaultBaseUrl}</SecretSyncLabel>
<SecretSyncLabel label="Label">{label}</SecretSyncLabel>
</>
);
};
@@ -0,0 +1,12 @@
import { useFormContext } from "react-hook-form";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const AzureKeyVaultSyncReviewFields = () => {
const { watch } = useFormContext<TSecretSyncForm & { destination: SecretSync.AzureKeyVault }>();
const vaultBaseUrl = watch("destinationConfig.vaultBaseUrl");
return <SecretSyncLabel label="Vault URL">{vaultBaseUrl}</SecretSyncLabel>;
};
@@ -9,6 +9,8 @@ import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/hel
import { SecretSync } from "@app/hooks/api/secretSyncs";
import { AwsParameterStoreSyncReviewFields } from "./AwsParameterStoreSyncReviewFields";
import { AzureAppConfigurationSyncReviewFields } from "./AzureAppConfigurationSyncReviewFields";
import { AzureKeyVaultSyncReviewFields } from "./AzureKeyVaultSyncReviewFields";
import { GcpSyncReviewFields } from "./GcpSyncReviewFields";
import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields";
@@ -46,6 +48,12 @@ export const SecretSyncReviewFields = () => {
case SecretSync.GCPSecretManager:
DestinationFieldsComponent = <GcpSyncReviewFields />;
break;
case SecretSync.AzureKeyVault:
DestinationFieldsComponent = <AzureKeyVaultSyncReviewFields />;
break;
case SecretSync.AzureAppConfiguration:
DestinationFieldsComponent = <AzureAppConfigurationSyncReviewFields />;
break;
default:
throw new Error(`Unhandled Destination Review Fields: ${destination}`);
}
@@ -0,0 +1,19 @@
import { z } from "zod";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const AzureAppConfigurationSyncDestinationSchema = z.object({
destination: z.literal(SecretSync.AzureAppConfiguration),
destinationConfig: z.object({
configurationUrl: z
.string()
.trim()
.min(1, { message: "Azure App Configuration URL is required" })
.url()
.refine(
(val) => val.endsWith(".azconfig.io"),
"URL should have the following format: https://resource-name-here.azconfig.io"
),
label: z.string().optional()
})
});
@@ -0,0 +1,10 @@
import { z } from "zod";
import { SecretSync } from "@app/hooks/api/secretSyncs";
export const AzureKeyVaultSyncDestinationSchema = z.object({
destination: z.literal(SecretSync.AzureKeyVault),
destinationConfig: z.object({
vaultBaseUrl: z.string().min(1, "Vault Base URL required")
})
});
@@ -6,6 +6,8 @@ import { SecretSyncInitialSyncBehavior } from "@app/hooks/api/secretSyncs";
import { slugSchema } from "@app/lib/schemas";
import { AwsParameterStoreSyncDestinationSchema } from "./aws-parameter-store-sync-destination-schema";
import { AzureAppConfigurationSyncDestinationSchema } from "./azure-app-configuration-sync-destination-schema";
import { AzureKeyVaultSyncDestinationSchema } from "./azure-key-vault-sync-destination-schema";
import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema";
const BaseSecretSyncSchema = z.object({
@@ -35,7 +37,9 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [
AwsParameterStoreSyncDestinationSchema,
AwsSecretsManagerSyncDestinationSchema,
GitHubSyncDestinationSchema,
GcpSyncDestinationSchema
GcpSyncDestinationSchema,
AzureKeyVaultSyncDestinationSchema,
AzureAppConfigurationSyncDestinationSchema
]);
export const SecretSyncFormSchema = SecretSyncUnionSchema.and(BaseSecretSyncSchema);